use crate::{Item, ItemDetails, PackList, Spaced};
use alloc::{string::ToString, vec::Vec};
use core::fmt;

#[derive(Debug, PartialEq, Eq)]
#[cfg_attr(feature = "std", derive(thiserror::Error))]
pub enum Error {
    /// ```
    /// use zhed_packlist::{PackList, ParseError};
    /// assert_eq!("".parse::<PackList>(), Err(ParseError::UnexpectedEof));
    /// ```
    #[cfg_attr(feature = "std", error("unexpected EOF"))]
    UnexpectedEof,
}

impl core::str::FromStr for PackList {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Error> {
        let mut it = s.lines().map(|i| i.trim_end());

        // 1st line: packlist name
        let name = it.next().ok_or(Error::UnexpectedEof)?;

        // to-pack items
        let items = it
            .map(|line| match line.parse() {
                Ok(x) => x,
                Err(x) => match x {},
            })
            .collect();

        Ok(PackList {
            name: name.to_string(),
            items,
        })
    }
}

impl core::str::FromStr for Item {
    type Err = core::convert::Infallible;

    fn from_str(line: &str) -> Result<Self, core::convert::Infallible> {
        let (det, text) = parse_itemdet(line)
            .map(|(det, rest)| (Some(det), rest))
            .unwrap_or((None, line));
        Ok(Self {
            det,
            text: text.to_string(),
        })
    }
}

fn parse_itemdet(mut line: &str) -> Option<(ItemDetails, &str)> {
    if line.len() < 5 {
        return None;
    }
    let mut ret = ItemDetails {
        markers: Vec::new(),
        multiplier: None,
    };

    // parse markers
    while let Some(x) = line.strip_prefix('[') {
        let mut it = x.chars();
        let a = it.next()?;
        let b = it.next()?;
        if ']' != it.next()? {
            return None;
        }
        line = it.as_str();
        let wsl = it
            .take_while(|i| i.is_whitespace())
            .map(|i| i.len_utf8())
            .sum::<usize>();
        let (space, rest) = line.split_at(wsl);
        ret.markers.push(Spaced {
            val: [a, b],
            space: space.to_string(),
        });
        line = rest;
    }
    if ret.markers.is_empty() {
        return None;
    }

    if let Some((val, space, rest)) = parse_multiplier(line) {
        ret.multiplier = Some(Spaced {
            val,
            space: space.to_string(),
        });
        line = rest;
    }

    Some((ret, line))
}

fn parse_multiplier(line: &str) -> Option<(u32, &str, &str)> {
    if !line.starts_with(|i: char| i.is_ascii_digit()) {
        return None;
    }
    let mut multiplier = 0;
    let mut it = line.chars();
    loop {
        match it.next() {
            Some('x') => {
                let tmp = it.as_str();
                let wsl = it
                    .take_while(|i| i.is_whitespace())
                    .map(|i| i.len_utf8())
                    .sum::<usize>();
                let (a, b) = tmp.split_at(wsl);
                return Some((multiplier, a, b));
            }
            Some(i) if i.is_ascii_digit() => {
                multiplier *= 10;
                multiplier += u32::from((i as u8) - b'0');
            }
            _ => return None,
        }
    }
}

impl fmt::Display for PackList {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        writeln!(f, "{}", self.name.trim_end())?;
        for item in &self.items {
            writeln!(f, "{}", item)?;
        }
        Ok(())
    }
}

impl fmt::Display for Item {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if let Some(x) = &self.det {
            fmt::Display::fmt(x, f)?;
        }
        f.write_str(self.text.trim_end())
    }
}

impl fmt::Display for ItemDetails {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        for m in &self.markers {
            write!(f, "[{}{}]{}", m.val[0], m.val[1], m.space)?;
        }
        if let Some(x) = &self.multiplier {
            write!(f, "{}x{}", x.val, x.space)?;
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use alloc::{format, string::String, vec};
    use proptest::prelude::*;

    #[test]
    fn ex1() {
        let inp = r#"Packliste Ölland:
[* ] [ _] [. ] 132x Ruß
"#;
        let res = PackList {
            name: "Packliste Ölland:".to_string(),
            items: vec![Item {
                det: Some(ItemDetails {
                    markers: vec![
                        Spaced {
                            val: ['*', ' '],
                            space: " ".to_string(),
                        },
                        Spaced {
                            val: [' ', '_'],
                            space: " ".to_string(),
                        },
                        Spaced {
                            val: ['.', ' '],
                            space: " ".to_string(),
                        },
                    ],
                    multiplier: Some(Spaced {
                        val: 132,
                        space: " ".to_string(),
                    }),
                }),
                text: "Ruß".to_string(),
            }],
        };
        assert_eq!(inp.parse::<PackList>().unwrap(), res);
        assert_eq!(res.to_string(), inp);
    }

    proptest! {
        #[test]
        fn doesnt_crash(s in "\\PC*") {
            let _ = s.parse::<PackList>();
        }

        #[test]
        fn correctly_trimmed(s in "(?s:.+)") {
            let s2: String = s.lines().flat_map(|i| i.trim_end().chars().chain(core::iter::once('\n'))).collect();
            let x1 = s.parse::<PackList>().unwrap();
            let x2 = s2.parse::<PackList>().unwrap();
            prop_assert_eq!(x1, x2);
        }

        #[test]
        fn roundtrip(s in "(?s:[^\r]+\n)".prop_filter("line ends are trimmed", |v| v.lines().all(|l| l == l.trim_end()))) {
            prop_assert_eq!(&s, &s.parse::<PackList>().unwrap().to_string());
        }
    }
}