use crate::error::Error;
use crate::error::InvalidArgument;
use crate::error::ParseError;
use crate::time::TimeStamp;
use crate::track::Disc;
use crate::track::Index;
use crate::track::Track;
use crate::utils;
use crate::Cuna;
use std::fmt;
use std::iter::Enumerate;
use std::str::Lines;
pub type Parser<'a> = Parna<Enumerate<Lines<'a>>>;
macro_rules! fail {
(token $token: expr) => {
return Err($crate::error::ParseError::unexpected_token($token))
};
(syntax $cmd: expr, $msg: expr) => {
return Err($crate::error::ParseError::syntax_error($cmd, $msg))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Command<'a> {
Rem(&'a str),
Title(&'a str),
Performer(&'a str),
Songwriter(&'a str),
Catalog(u64),
Cdtextfile(&'a str),
File(&'a str, &'a str),
Track(u8, &'a str),
Index(u8, TimeStamp),
Pregap(&'a str),
Postgap(&'a str),
Isrc(&'a str),
Flags(&'a str),
Empty,
}
#[derive(Debug, Clone)]
pub struct Parna<I>(I);
impl<'a> Command<'a> {
pub fn new(s: &'a str) -> Result<Self, ParseError> {
let s = match s.trim() {
"" => return Ok(Self::Empty),
ts => ts,
};
let (content, command) = match utils::token(s) {
Ok(ok) => ok,
Err(_) => fail!(syntax s, "missing arguments"),
};
match command.to_ascii_lowercase().as_ref() {
"rem" => Ok(Self::Rem(content)),
"title" => Ok(Self::Title(trimq(content))),
"performer" => Ok(Self::Performer(trimq(content))),
"songwriter" => Ok(Self::Songwriter(trimq(content))),
"catalog" => match utils::number(13)(content) {
Ok((_, catalog)) => Ok(Self::Catalog(catalog)),
Err(_) => fail!(syntax content, "invaild catalog"),
},
"cdtextfile" => Ok(Self::Cdtextfile(trimq(content))),
"file" => match utils::quote_opt(content) {
Ok(("", _)) | Err(_) => Err(InvalidArgument::MissingArgument.into()),
Ok((format, path)) => Ok(Self::File(trimq(path), format.trim())),
},
"track" => match utils::token(content) {
Ok((format, id)) => Ok(Self::Track(parse_id(id)?, format)),
Err(_) => Err(InvalidArgument::MissingArgument.into()),
},
"index" => match utils::token(content) {
Ok((timestamp, id)) => Ok(Self::Index(parse_id(id)?, timestamp.parse()?)),
Err(_) => Err(InvalidArgument::MissingArgument.into()),
},
"pregap" => Ok(Self::Pregap(trimq(content))),
"postgap" => Ok(Self::Postgap(trimq(content))),
"isrc" => Ok(Self::Isrc(trimq(content))),
"flags" => Ok(Self::Flags(trimq(content))),
_ => Err(ParseError::unexpected_token(command)),
}
}
pub fn parse(&self, sheet: &mut Cuna) -> Result<(), ParseError> {
match *self {
Self::Empty => {}
Self::Rem(s) => sheet.comments.push(s.to_owned()),
Self::Title(s) => match sheet.last_track_mut() {
Some(tk) => tk.push_title(s.to_owned()),
None => sheet.header.push_title(s.to_owned()),
},
Self::Performer(s) => match sheet.last_track_mut() {
Some(tk) => tk.push_performer(s.to_owned()),
_ => sheet.header.push_performer(s.to_owned()),
},
Self::Songwriter(s) => match sheet.last_track_mut() {
Some(tk) => tk.push_songwriter(s.to_owned()),
_ => sheet.header.push_songwriter(s.to_owned()),
},
Self::Catalog(s) => match sheet.header.catalog {
None => sheet.header.catalog = Some(s),
_ => fail!(syntax self, "multiple `CATALOG` commands is not allowed"),
},
Self::Cdtextfile(s) => {
sheet.header.set_cdtextfile(s.to_owned());
}
Self::File(name, format) => {
sheet.push_file(Disc::new(name.to_owned(), format.to_owned()));
}
Self::Track(id, format) => match sheet.last_file_mut() {
Some(tk) => tk.push_track(Track::new_unchecked(id, format.to_owned())),
None => fail!(token "TRACK"),
},
Self::Index(id, timestamp) => match sheet.last_track_mut() {
Some(tk) if tk.postgap.is_none() => {
tk.push_index(Index::new_unchecked(id, timestamp))
}
Some(_) => fail!(syntax self, "Command `INDEX` should be before `POSTGAP`"),
None => fail!(token "INDEX"),
},
Self::Pregap(timestamp) => match sheet.last_track_mut() {
Some(tk) if tk.index.is_empty() && tk.pregap.is_none() => {
tk.set_pregep(timestamp.parse()?);
}
Some(tk) if tk.pregap.is_some() => {
fail!(syntax self, "Multiple `PREGAP` commands are not allowed in one `TRACK` scope")
}
Some(_) => fail!(syntax self, "Command `PREGAP` should be before `INDEX`"),
_ => fail!(token "PREGAP"),
},
Self::Postgap(timestamp) => match sheet.last_track_mut() {
Some(tk) if tk.postgap.is_none() => {
tk.set_postgep(timestamp.parse()?);
}
Some(_) => {
fail!(syntax self, "Multiple `POSTGAP` commands are not allowed in one `TRACK` scope")
}
None => fail!(token "POSTGAP"),
},
Self::Isrc(s) => match sheet.last_track_mut() {
Some(tk) if tk.isrc.is_none() => {
tk.set_isrc(s.to_owned());
}
Some(_) => {
fail!(syntax self, "Multiple `ISRC` commands are not allowed in one `TRACK` scope")
}
None => fail!(token "ISRC"),
},
Self::Flags(s) => match sheet.last_track_mut() {
Some(tk) if tk.flags.is_empty() => tk.push_flags(s.split(' ')),
Some(_) => {
fail!(syntax self, "Multiple `FLAGS` commands are not allowed in one `TRACK` scope")
}
None => fail!(token "FLAGS"),
},
}
Ok(())
}
}
impl fmt::Display for Command<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::Rem(c) => write!(formatter, "REM {}", c),
Self::Title(c) => write!(formatter, r#"TITLE "{}""#, c),
Self::Performer(c) => write!(formatter, r#"PERFORMER "{}""#, c),
Self::Songwriter(c) => write!(formatter, r#"SONGWRITER "{}""#, c),
Self::Catalog(c) => write!(formatter, "CATALOG {}", c),
Self::Cdtextfile(c) => write!(formatter, r#"CDTEXTFILE "{}""#, c),
Self::File(name, tp) => write!(formatter, r#"FILE "{}" {}"#, name, tp),
Self::Track(id, format) => write!(formatter, "TRACK {} {}", id, format),
Self::Index(id, timestamp) => write!(formatter, "INDEX {} {}", id, timestamp),
Self::Pregap(c) => write!(formatter, "PREGAP {}", c),
Self::Postgap(c) => write!(formatter, "POSTGAP {}", c),
Self::Isrc(c) => write!(formatter, "ISRC {}", c),
Self::Flags(c) => write!(formatter, "FLAG {}", c),
Self::Empty => Ok(()),
}
}
}
impl<'a> Parna<Enumerate<Lines<'a>>> {
pub fn new(s: &'a str) -> Self {
Self(s.lines().enumerate())
}
}
impl<'a, I: Iterator<Item = &'a str>> Parna<Enumerate<I>> {
pub fn from_lines(lines: I) -> Self {
Self(lines.enumerate())
}
#[deprecated]
pub fn set_lines(&mut self, lines: I) {
self.0 = lines.enumerate();
}
}
impl<'a, I: Iterator<Item = (usize, &'a str)>> Parna<I> {
pub fn with_iter(it: I) -> Self {
Self(it)
}
pub fn data(&mut self) -> &mut I {
self.0.by_ref()
}
pub fn parse_next_line(&mut self, state: &mut Cuna) -> Result<(), Error> {
self.parse_next_n_lines(1, state)
}
pub fn parse_next_n_lines(&mut self, n: usize, state: &mut Cuna) -> Result<(), Error> {
for (at, line) in self.0.by_ref().take(n) {
let to_error = |e| Error::new(e, at + 1);
Command::new(line)
.map_err(to_error)?
.parse(state)
.map_err(to_error)?;
}
Ok(())
}
pub fn parse(&mut self, state: &mut Cuna) -> Result<(), Error> {
for (at, line) in self.0.by_ref() {
let to_error = |e| Error::new(e, at + 1);
Command::new(line)
.map_err(to_error)?
.parse(state)
.map_err(to_error)?;
}
Ok(())
}
}
impl<'a, I: Iterator<Item = (usize, &'a str)> + Clone> Parna<I> {
pub fn current_line(&self) -> Option<&'a str> {
self.current().map(|(_, s)| s)
}
pub fn current(&self) -> Option<(usize, &'a str)> {
self.0.clone().next()
}
}
#[inline(always)]
fn parse_id(s: &str) -> Result<u8, InvalidArgument> {
Ok(utils::number(2)(s)
.map_err(|_| InvalidArgument::InvalidId)?
.1)
}
#[inline(always)]
fn trimq(s: &str) -> &str {
s.trim_matches('"')
}