use crate::model::Line;
use nom::{
    bytes::complete::{is_not, take, take_until},
    character::complete::{char, space0},
    combinator::{opt, peek},
    error::{Error, ErrorKind},
    multi::separated_list1,
    sequence::delimited,
    IResult, Parser,
};

// Temporary API
// Only expose parse_next ATM, so that tracking line number is done outside of the parser.

pub fn parse_line(s: &str) -> IResult<&str, Option<Line>> {
    let (s, _) = space0(s)?;
    let (s, line) = opt(parse_parts).parse(s)?;
    let (s, _) = parse_end_of_line(s)?;
    Ok((s, line))
}

fn parse_parts(s: &str) -> IResult<&str, Line> {
    let (s, part_1) = parse_options(s)?;
    let (s, _) = sep(':')(s)?;
    let (s, part_2) = parse_options(s)?;
    Ok((s, Line { part_1, part_2 }))
}

// Rest

fn parse_options(s: &str) -> IResult<&str, Vec<String>> {
    separated_list1(sep('|'), parse_term).parse(s)
}

pub fn parse_term(s: &str) -> IResult<&str, String> {
    let mut term = String::from("");
    let mut s = s;

    while let Ok((_, c)) = peek(take::<usize, &str, ()>(1_usize)).parse(s) {
        if c == "[" {
            let (s1, cs) = take_until("]")(s)?;
            s = s1;
            term.push_str(cs);
        } else if c == "(" {
            let (s1, cs) = take_until(")")(s)?;
            s = s1;
            term.push_str(cs);
        } else if c == ":" || c == "|" || c == "#" || c == "\n" || c == "\r" {
            break;
        } else {
            let (s1, cs) = is_not("[(:|#\n\r")(s)?;
            s = s1;
            term.push_str(cs);
        }
    }

    if term.is_empty() {
        Err(nom::Err::Error(Error {
            input: s,
            code: ErrorKind::Fail,
        }))
    } else {
        Ok((s, term.trim().to_string()))
    }
}

fn parse_end_of_line(s: &str) -> IResult<&str, ()> {
    let (s, _) = space0(s)?;
    let (s, _) = opt(parse_comment).parse(s)?;
    Ok((s, ()))
}

fn parse_comment(s: &str) -> IResult<&str, ()> {
    let (s, _) = char('#')(s)?;
    let (s, _) = is_not("\n\r")(s)?;
    Ok((s, ()))
}

// Helpers

pub fn sep(c: char) -> impl FnMut(&str) -> IResult<&str, ()> {
    move |s: &str| {
        let (s, _) = delimited(space0, char(c), space0).parse(s)?;
        Ok((s, ()))
    }
}

// Tests

#[cfg(test)]
mod tests {

    use super::*;
    use nom::{character::complete::newline, multi::many0};

    fn parse(s: &str) -> IResult<&str, Vec<Line>> {
        let (s, lines) = many0(parse_next).parse(s)?;
        let (s, _) = many0(parse_empty_line).parse(s)?;
        Ok((s, lines))
    }

    fn parse_next(s: &str) -> IResult<&str, Line> {
        let (s, _) = many0(parse_empty_line).parse(s)?;
        let (s, _) = space0(s)?;
        let (s, part_1) = parse_options(s)?;
        let (s, _) = sep(':')(s)?;
        let (s, part_2) = parse_options(s)?;
        let (s, _) = parse_end_of_line(s)?;
        let (s, _) = opt(newline).parse(s)?;
        Ok((s, Line { part_1, part_2 }))
    }

    fn parse_empty_line(s: &str) -> IResult<&str, ()> {
        let (s, _) = parse_end_of_line(s)?;
        let (s, _) = newline(s)?;
        Ok((s, ()))
    }

    #[test]
    fn simple() {
        assert_eq!(parse("foo : bar"), lines(vec!((vec!("foo"), vec!("bar")))))
    }

    #[test]
    fn spaces() {
        assert_eq!(
            parse("     foo     :      bar      "),
            lines(vec!((vec!("foo"), vec!("bar"))))
        )
    }

    #[test]
    fn comments() {
        assert_eq!(
            parse("foo : bar # This is a comment"),
            lines(vec!((vec!("foo"), vec!("bar"))))
        )
    }

    #[test]
    fn options() {
        assert_eq!(
            parse("foo | bar | baz : A | B | C"),
            lines(vec!((vec!("foo", "bar", "baz"), vec!("A", "B", "C"))))
        )
    }

    #[test]
    fn term_with_spaces() {
        assert_eq!(
            parse("foo bar : baz baz"),
            lines(vec!((vec!("foo bar"), vec!("baz baz"))))
        )
    }

    #[test]
    fn paren() {
        assert_eq!(
            parse("foo (|:[) : bar [::|)]"),
            lines(vec!((vec!("foo (|:[)"), vec!("bar [::|)]"))))
        )
    }

    #[test]
    fn empty_lines() {
        assert_eq!(parse("\n \n   \n# Hello\n     # Test\n\n\n"), lines(vec!()))
    }

    #[test]
    fn multi_lines() {
        assert_eq!(
            parse("foo : FOO\nbar : BAR\nbaz : BAZ"),
            lines(vec!(
                (vec!("foo"), vec!("FOO")),
                (vec!("bar"), vec!("BAR")),
                (vec!("baz"), vec!("BAZ"))
            ))
        )
    }

    // Helpers

    static EMPTY_STRING: &str = "";

    fn lines(lines: Vec<(Vec<&str>, Vec<&str>)>) -> IResult<&'static str, Vec<Line>> {
        Ok((
            EMPTY_STRING,
            lines
                .into_iter()
                .map(|line| to_line(line.0, line.1))
                .collect(),
        ))
    }

    fn to_line(part_1: Vec<&str>, part_2: Vec<&str>) -> Line {
        Line {
            part_1: part_1.into_iter().map(|s| s.to_string()).collect(),
            part_2: part_2.into_iter().map(|s| s.to_string()).collect(),
        }
    }
}