use super::stream::Stream;
use crate::ast::{
    Build, DefaultOutput, DynamicBuild, DynamicModule, Include, Module, Rule, Statement, Submodule,
    VariableDefinition,
};
use combine::{
    attempt, choice, eof, many, many1, none_of, not_followed_by, one_of, optional,
    parser::char::{alpha_num, char, letter, newline, string},
    value, Parser,
};

const OPERATOR_CHARACTERS: &[char] = &['|', ':'];
const DYNAMIC_MODULE_VERSION_VARIABLE: &str = "ninja_dyndep_version";

pub fn module<'a>() -> impl Parser<Stream<'a>, Output = Module> {
    (optional(line_break()), many(statement()))
        .skip(eof())
        .map(|(_, statements)| Module::new(statements))
}

pub fn dynamic_module<'a>() -> impl Parser<Stream<'a>, Output = DynamicModule> {
    (
        optional(line_break()),
        dynamic_module_version(),
        many(dynamic_build()),
    )
        .skip(eof())
        .map(|(_, _, builds)| DynamicModule::new(builds))
}

fn statement<'a>() -> impl Parser<Stream<'a>, Output = Statement> {
    choice((
        build().map(Statement::from),
        default().map(Statement::from),
        include().map(Statement::from),
        rule().map(Statement::from),
        submodule().map(Statement::from),
        variable_definition().map(Statement::from),
    ))
}

fn variable_definition<'a>() -> impl Parser<Stream<'a>, Output = VariableDefinition> {
    (
        attempt(identifier().skip(sign("="))),
        optional(string_line()),
    )
        .skip(line_break())
        .map(|(name, value)| VariableDefinition::new(name, value.unwrap_or_default()))
}

fn dynamic_module_version<'a>() -> impl Parser<Stream<'a>, Output = String> {
    attempt((keyword(DYNAMIC_MODULE_VERSION_VARIABLE), sign("=")))
        .with(string_line())
        .skip(line_break())
}

fn rule<'a>() -> impl Parser<Stream<'a>, Output = Rule> {
    (
        keyword("rule"),
        identifier(),
        line_break(),
        (indent(), keyword("command"), sign("="))
            .with(string_line())
            .skip(line_break()),
        optional(
            (indent(), keyword("description"), sign("="))
                .with(string_line())
                .skip(line_break()),
        ),
    )
        .map(|(_, name, _, command, description)| Rule::new(name, command, description))
        .expected("rule statement")
}

fn build<'a>() -> impl Parser<Stream<'a>, Output = Build> {
    (
        keyword("build"),
        many1(string_literal()),
        optional(sign("|").with(many1::<Vec<_>, _, _>(string_literal()))),
        sign(":"),
        identifier(),
        many(string_literal()),
        optional(sign("|").with(many1::<Vec<_>, _, _>(string_literal()))),
        optional(sign("||").with(many1::<Vec<_>, _, _>(string_literal()))),
        line_break(),
        many(indent().with(variable_definition())),
    )
        .map(
            |(
                _,
                outputs,
                implicit_outputs,
                _,
                rule,
                inputs,
                implicit_inputs,
                order_only_inputs,
                _,
                variable_definitions,
            )| {
                Build::new(
                    outputs,
                    implicit_outputs.into_iter().flatten().collect(),
                    rule,
                    inputs,
                    implicit_inputs.into_iter().flatten().collect(),
                    order_only_inputs.into_iter().flatten().collect(),
                    variable_definitions,
                )
            },
        )
        .expected("build statement")
}

pub fn dynamic_build<'a>() -> impl Parser<Stream<'a>, Output = DynamicBuild> {
    (
        keyword("build"),
        string_literal(),
        sign(":"),
        keyword("dyndep"),
        optional(sign("|").with(many1::<Vec<_>, _, _>(string_literal()))),
        line_break(),
    )
        .map(|(_, output, _, _, implicit_inputs, _)| {
            DynamicBuild::new(output, implicit_inputs.into_iter().flatten().collect())
        })
        .expected("build statement")
}

fn default<'a>() -> impl Parser<Stream<'a>, Output = DefaultOutput> {
    (keyword("default"), many1(string_literal()))
        .skip(line_break())
        .map(|(_, outputs)| DefaultOutput::new(outputs))
        .expected("default statement")
}

fn include<'a>() -> impl Parser<Stream<'a>, Output = Include> {
    (keyword("include"), string_line())
        .skip(line_break())
        .map(|(_, path)| Include::new(path))
        .expected("include statement")
}

fn submodule<'a>() -> impl Parser<Stream<'a>, Output = Submodule> {
    (keyword("subninja"), string_line())
        .skip(line_break())
        .map(|(_, path)| Submodule::new(path))
        .expected("subninja statement")
}

fn string_line<'a>() -> impl Parser<Stream<'a>, Output = String> {
    many1(none_of(['\n'])).map(|string: String| string.trim().into())
}

fn string_literal<'a>() -> impl Parser<Stream<'a>, Output = String> {
    token(many1(none_of(
        [' ', '\t', '\r', '\n']
            .into_iter()
            .chain(OPERATOR_CHARACTERS.iter().cloned()),
    )))
}

fn keyword<'a>(name: &'static str) -> impl Parser<Stream<'a>, Output = ()> {
    token(attempt(string(name).skip(not_followed_by(alpha_num()))))
        .with(value(()))
        .expected(name)
}

fn identifier<'a>() -> impl Parser<Stream<'a>, Output = String> {
    token(
        (
            choice((letter(), char('_'))),
            many(choice((alpha_num(), char('_')))),
        )
            .map(|(head, tail): (_, String)| [head.into(), tail].concat()),
    )
    .expected("identifier")
}

fn sign<'a>(sign: &'static str) -> impl Parser<Stream<'a>, Output = ()> {
    attempt(token(string(sign)).skip(not_followed_by(one_of(OPERATOR_CHARACTERS.iter().cloned()))))
        .with(value(()))
        .expected(sign)
}

fn token<'a, O, P: Parser<Stream<'a>, Output = O>>(
    parser: P,
) -> impl Parser<Stream<'a>, Output = O> {
    parser.skip(blank())
}

fn indent<'a>() -> impl Parser<Stream<'a>, Output = ()> {
    many1::<Vec<_>, _, _>(char(' '))
        .with(value(()))
        .expected("indent")
}

fn blank<'a>() -> impl Parser<Stream<'a>, Output = ()> {
    many::<Vec<_>, _, _>(one_of([' ', '\t', '\r'])).with(value(()))
}

fn line_break<'a>() -> impl Parser<Stream<'a>, Output = ()> {
    many1::<Vec<_>, _, _>(attempt((blank(), newline()))).with(value(()))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::parse::stream::stream;

    fn explicit_build(
        outputs: Vec<String>,
        rule: impl Into<String>,
        inputs: Vec<String>,
        variable_definitions: Vec<VariableDefinition>,
    ) -> Build {
        Build::new(
            outputs,
            vec![],
            rule,
            inputs,
            vec![],
            vec![],
            variable_definitions,
        )
    }

    #[test]
    fn parse_module() {
        assert_eq!(module().parse(stream("")).unwrap().0, Module::new(vec![]));
        assert_eq!(
            module().parse(stream("x = 42\n")).unwrap().0,
            Module::new(vec![VariableDefinition::new("x", "42").into()])
        );
        assert_eq!(
            module().parse(stream("x = 1\ny = 2\n")).unwrap().0,
            Module::new(vec![
                VariableDefinition::new("x", "1").into(),
                VariableDefinition::new("y", "2").into(),
            ],)
        );
        assert_eq!(
            module()
                .parse(stream("rule foo\n command = bar\n"))
                .unwrap()
                .0,
            Module::new(vec![Rule::new("foo", "bar", None).into()])
        );
        assert_eq!(
            module()
                .parse(stream(
                    "rule foo\n command = bar\nrule baz\n command = blah\n"
                ))
                .unwrap()
                .0,
            Module::new(vec![
                Rule::new("foo", "bar", None).into(),
                Rule::new("baz", "blah", None).into(),
            ],)
        );
        assert_eq!(
            module().parse(stream("builddir = foo\n")).unwrap().0,
            Module::new(vec![VariableDefinition::new("builddir", "foo").into()])
        );
    }

    #[test]
    fn parse_dynamic_module() {
        assert_eq!(
            dynamic_module()
                .parse(stream("ninja_dyndep_version = 1\n"))
                .unwrap()
                .0,
            DynamicModule::new(vec![])
        );
        assert_eq!(
            dynamic_module()
                .parse(stream("ninja_dyndep_version = 1\nbuild foo: dyndep\n"))
                .unwrap()
                .0,
            DynamicModule::new(vec![DynamicBuild::new("foo", vec![])])
        );
        assert_eq!(
            dynamic_module()
                .parse(stream(
                    "ninja_dyndep_version = 1\nbuild foo: dyndep\nbuild bar: dyndep\n"
                ))
                .unwrap()
                .0,
            DynamicModule::new(vec![
                DynamicBuild::new("foo", vec![]),
                DynamicBuild::new("bar", vec![])
            ])
        );
    }

    #[test]
    fn parse_variable_definition() {
        assert_eq!(
            variable_definition().parse(stream("x = 42\n")).unwrap().0,
            VariableDefinition::new("x", "42")
        );
        assert_eq!(
            variable_definition()
                .parse(stream("foo = 1 + 1\n"))
                .unwrap()
                .0,
            VariableDefinition::new("foo", "1 + 1")
        );
        assert_eq!(
            variable_definition().parse(stream("x =\n")).unwrap().0,
            VariableDefinition::new("x", "")
        );
        assert_eq!(
            variable_definition().parse(stream("x = \n")).unwrap().0,
            VariableDefinition::new("x", "")
        );
    }

    #[test]
    fn parse_dynamic_module_version() {
        assert_eq!(
            dynamic_module_version()
                .parse(stream("ninja_dyndep_version = 42\n"))
                .unwrap()
                .0,
            "42".to_string(),
        );
    }

    #[test]
    fn parse_rule() {
        assert_eq!(
            rule()
                .parse(stream("rule foo\n command = bar\n"))
                .unwrap()
                .0,
            Rule::new("foo", "bar", None)
        );
        assert_eq!(
            rule()
                .parse(stream("rule foo\n command = bar\n description = baz\n"))
                .unwrap()
                .0,
            Rule::new("foo", "bar", Some("baz".into()))
        );
    }

    #[test]
    fn parse_build() {
        assert_eq!(
            build().parse(stream("build foo: bar\n")).unwrap().0,
            explicit_build(vec!["foo".into()], "bar", vec![], vec![])
        );
        assert_eq!(
            build().parse(stream("build foo: bar baz\n")).unwrap().0,
            explicit_build(vec!["foo".into()], "bar", vec!["baz".into()], vec![])
        );
        assert_eq!(
            build()
                .parse(stream("build foo: bar baz blah\n"))
                .unwrap()
                .0,
            explicit_build(
                vec!["foo".into()],
                "bar",
                vec!["baz".into(), "blah".into()],
                vec![]
            )
        );
        assert_eq!(
            build().parse(stream("build foo bar: baz\n")).unwrap().0,
            explicit_build(vec!["foo".into(), "bar".into()], "baz", vec![], vec![])
        );
        assert_eq!(
            build().parse(stream("build foo: bar\n x = 1\n")).unwrap().0,
            explicit_build(
                vec!["foo".into()],
                "bar",
                vec![],
                vec![VariableDefinition::new("x", "1")]
            )
        );
        assert_eq!(
            build()
                .parse(stream("build foo: bar\n x = 1\n y = 2\n"))
                .unwrap()
                .0,
            explicit_build(
                vec!["foo".into()],
                "bar",
                vec![],
                vec![
                    VariableDefinition::new("x", "1"),
                    VariableDefinition::new("y", "2")
                ]
            )
        );
        assert_eq!(
            build().parse(stream("build x1 | x2: rule\n")).unwrap().0,
            Build::new(
                vec!["x1".into()],
                vec!["x2".into()],
                "rule",
                vec![],
                vec![],
                vec![],
                vec![]
            )
        );
        assert_eq!(
            build().parse(stream("build x1 | x2 x3: rule\n")).unwrap().0,
            Build::new(
                vec!["x1".into()],
                vec!["x2".into(), "x3".into()],
                "rule",
                vec![],
                vec![],
                vec![],
                vec![]
            )
        );
        assert_eq!(
            build().parse(stream("build x1: rule | x2\n")).unwrap().0,
            Build::new(
                vec!["x1".into()],
                vec![],
                "rule",
                vec![],
                vec!["x2".into()],
                vec![],
                vec![]
            )
        );
        assert_eq!(
            build().parse(stream("build x1: rule | x2 x3\n")).unwrap().0,
            Build::new(
                vec!["x1".into()],
                vec![],
                "rule",
                vec![],
                vec!["x2".into(), "x3".into()],
                vec![],
                vec![]
            )
        );
        assert_eq!(
            build().parse(stream("build x1: rule || x2\n")).unwrap().0,
            Build::new(
                vec!["x1".into()],
                vec![],
                "rule",
                vec![],
                vec![],
                vec!["x2".into()],
                vec![],
            )
        );
        assert_eq!(
            build()
                .parse(stream("build x1: rule || x2 x3\n"))
                .unwrap()
                .0,
            Build::new(
                vec!["x1".into()],
                vec![],
                "rule",
                vec![],
                vec![],
                vec!["x2".into(), "x3".into()],
                vec![]
            )
        );
    }

    #[test]
    fn parse_dynamic_build() {
        assert_eq!(
            dynamic_build()
                .parse(stream("build foo: dyndep\n"))
                .unwrap()
                .0,
            DynamicBuild::new("foo", vec![])
        );
        assert_eq!(
            dynamic_build()
                .parse(stream("build foo: dyndep | bar\n"))
                .unwrap()
                .0,
            DynamicBuild::new("foo", vec!["bar".into()])
        );
        assert_eq!(
            dynamic_build()
                .parse(stream("build foo: dyndep | bar baz\n"))
                .unwrap()
                .0,
            DynamicBuild::new("foo", vec!["bar".into(), "baz".into()])
        );
    }

    #[test]
    fn parse_default() {
        assert!(default().parse(stream("")).is_err());
        assert!(default().parse(stream("default\n")).is_err());
        assert_eq!(
            default().parse(stream("default foo\n")).unwrap().0,
            DefaultOutput::new(vec!["foo".into()])
        );
        assert_eq!(
            default().parse(stream("default foo bar\n")).unwrap().0,
            DefaultOutput::new(vec!["foo".into(), "bar".into()])
        );
    }

    #[test]
    fn parse_include() {
        assert_eq!(
            include().parse(stream("include foo\n")).unwrap().0,
            Include::new("foo")
        );
    }

    #[test]
    fn parse_submodule() {
        assert_eq!(
            submodule().parse(stream("subninja foo\n")).unwrap().0,
            Submodule::new("foo")
        );
    }

    #[test]
    fn parse_string_line() {
        assert!(string_line().parse(stream("")).is_err());
        assert_eq!(
            string_line().parse(stream("foo")).unwrap().0,
            "foo".to_string()
        );
        assert_eq!(
            string_line().parse(stream("foo\n")).unwrap().0,
            "foo".to_string()
        );
        assert_eq!(
            string_line().parse(stream("foo \n")).unwrap().0,
            "foo".to_string()
        );
        assert_eq!(
            string_line().parse(stream("foo bar")).unwrap().0,
            "foo bar".to_string()
        );
    }

    #[test]
    fn parse_string_literal() {
        assert!(string_literal().parse(stream("")).is_err());
        assert_eq!(
            string_literal().parse(stream("foo")).unwrap().0,
            "foo".to_string()
        );
        assert_eq!(
            string_literal().parse(stream("foo bar")).unwrap().0,
            "foo".to_string()
        );
    }

    #[test]
    fn parse_keyword() {
        assert!(keyword("foo").parse(stream("foo")).is_ok());
        assert!(keyword("fo").parse(stream("foo")).is_err());
    }

    #[test]
    fn parse_identifier() {
        assert_eq!(
            identifier().parse(stream("foo")).unwrap().0,
            "foo".to_string()
        );
        assert_eq!(
            identifier().parse(stream("foo bar")).unwrap().0,
            "foo".to_string()
        );
        assert_eq!(
            identifier().parse(stream("foo_bar")).unwrap().0,
            "foo_bar".to_string()
        );
        assert_eq!(
            identifier().parse(stream("_foo")).unwrap().0,
            "_foo".to_string()
        );
    }

    #[test]
    fn parse_blank() {
        assert!(blank().skip(eof()).parse(stream("")).is_ok());
        assert!(blank().skip(eof()).parse(stream(" ")).is_ok());
        assert!(blank().skip(eof()).parse(stream("\t")).is_ok());
        assert!(blank().skip(eof()).parse(stream("\r")).is_ok());
        assert!(blank().skip(eof()).parse(stream("  ")).is_ok());
        assert!(blank().skip(eof()).parse(stream(" \t")).is_ok());
        assert!(blank().skip(eof()).parse(stream("\n")).is_err());
        assert!(blank().skip(eof()).parse(stream(" \n")).is_err());
    }

    #[test]
    fn parse_line_break() {
        assert!(line_break().skip(eof()).parse(stream("")).is_err());
        assert!(line_break().skip(eof()).parse(stream("\n")).is_ok());
        assert!(line_break().skip(eof()).parse(stream(" \n")).is_ok());
        assert!(line_break().skip(eof()).parse(stream("  \n")).is_ok());
        assert!(line_break().skip(eof()).parse(stream("\n\n")).is_ok());
        assert!(line_break().skip(eof()).parse(stream("\n ")).is_err());
    }
}
