use crate::{
    ast,
    expr::UntypedExpr,
    parser::{annotation, error::ParseError, expr, pattern, token::Token, utils},
};
use chumsky::prelude::*;

pub fn parser() -> impl Parser<Token, ast::UntypedDefinition, Error = ParseError> {
    utils::optional_flag(Token::Pub)
        .then_ignore(just(Token::Fn))
        .then(select! {Token::Name {name} => name})
        .then(
            param(false)
                .separated_by(just(Token::Comma))
                .allow_trailing()
                .delimited_by(just(Token::LeftParen), just(Token::RightParen))
                .map_with_span(|arguments, span| (arguments, span)),
        )
        .then(just(Token::RArrow).ignore_then(annotation()).or_not())
        .then(
            expr::sequence()
                .or_not()
                .delimited_by(just(Token::LeftBrace), just(Token::RightBrace)),
        )
        .map_with_span(
            |((((public, name), (arguments, args_span)), return_annotation), body), span| {
                ast::UntypedDefinition::Fn(ast::Function {
                    arguments,
                    body: body.unwrap_or_else(|| UntypedExpr::todo(None, span)),
                    doc: None,
                    location: ast::Span {
                        start: span.start,
                        end: return_annotation
                            .as_ref()
                            .map(|l| l.location().end)
                            .unwrap_or_else(|| args_span.end),
                    },
                    end_position: span.end - 1,
                    name,
                    public,
                    return_annotation,
                    return_type: (),
                    on_test_failure: ast::OnTestFailure::FailImmediately,
                })
            },
        )
}

pub fn param(is_validator_param: bool) -> impl Parser<Token, ast::UntypedArg, Error = ParseError> {
    choice((
        select! {Token::Name {name} => name}
            .then(select! {Token::DiscardName {name} => name})
            .map_with_span(|(label, name), span| {
                ast::ArgBy::ByName(ast::ArgName::Discarded {
                    label,
                    name,
                    location: span,
                })
            }),
        select! {Token::DiscardName {name} => name}.map_with_span(|name, span| {
            ast::ArgBy::ByName(ast::ArgName::Discarded {
                label: name.clone(),
                name,
                location: span,
            })
        }),
        select! {Token::Name {name} => name}
            .then(select! {Token::Name {name} => name})
            .map_with_span(|(label, name), span| {
                ast::ArgBy::ByName(ast::ArgName::Named {
                    label,
                    name,
                    location: span,
                })
            }),
        select! {Token::Name {name} => name}.map_with_span(|name, span| {
            ast::ArgBy::ByName(ast::ArgName::Named {
                label: name.clone(),
                name,
                location: span,
            })
        }),
        pattern().map(ast::ArgBy::ByPattern),
    ))
    .then(just(Token::Colon).ignore_then(annotation()).or_not())
    .map_with_span(move |(by, annotation), span| ast::UntypedArg {
        location: span,
        annotation,
        doc: None,
        is_validator_param,
        by,
    })
}

#[cfg(test)]
mod tests {
    use crate::assert_definition;

    #[test]
    fn function_empty() {
        assert_definition!(
            r#"
            pub fn run() {}
            "#
        );
    }

    #[test]
    fn function_non_public() {
        assert_definition!(
            r#"
            fn run() {}
            "#
        );
    }

    #[test]
    fn function_assignment_only() {
        assert_definition!(
            r#"
            fn run() {
              let x = 1 + 1
            }
            "#
        );
    }

    #[test]
    fn function_by_pattern_no_annotation() {
        assert_definition!(
            r#"
            fn foo(Foo { my_field }) {
                my_field * 2
            }
            "#
        );
    }

    #[test]
    fn function_by_pattern_with_annotation() {
        assert_definition!(
            r#"
            fn foo(Foo { my_field }: Foo) {
                my_field * 2
            }
            "#
        );
    }
    #[test]
    fn function_by_pattern_with_alias() {
        assert_definition!(
            r#"
            fn foo(Foo { my_field, .. } as x) {
                my_field * x.my_other_field
            }
            "#
        );
    }

    #[test]
    fn grouping() {
        assert_definition!(
            r#"
            pub fn a0p(pair: Pair<ByteArray, Dict<ByteArray, Int>>) {
              (pair.2nd |> unsingleton).2nd
            }
            "#
        );
    }
}
