use crate::{
    ast::{Ast, AstAlloc, typ::Type},
    error::{ParseError, ParseErrors},
    files::FileId,
    identifier::LocIdent,
    position::RawSpan,
};

use lalrpop_util::lalrpop_mod;

lalrpop_mod!(
    #[allow(clippy::all)]
    #[allow(unused_parens)]
    #[allow(unused_imports)]
    pub grammar, "/grammar.rs");

use grammar::__ToTriple;

pub mod ast;
pub mod combine;
pub mod environment;
pub mod error;
pub mod files;
pub mod identifier;
pub mod lexer;
pub mod metrics;
pub mod position;
pub mod traverse;
pub mod typ;
pub(crate) mod uniterm;
pub mod utils;

#[cfg(test)]
mod tests;

/// Either an expression or a toplevel let declaration.
///
/// Used exclusively in the REPL to allow the defining of variables without having to specify `in`.
/// For example:
///
/// ```text
/// nickel>let foo = 1
/// nickel>foo
/// 1
/// ```
pub enum ExtendedTerm<T> {
    Term(T),
    ToplevelLet(LocIdent, T),
}

// The interface of LALRPOP-generated parsers, for each public rule. This trait is used as a facade
// to implement parser-independent features (such as error tolerance helpers), which don't have to
// be reimplemented for each and every parser. It's LALRPOP-specific and shouldn't be used outside
// of this module, if we don't want our implementation to be coupled to LALRPOP details.
//
// The type of `parse` was just copy-pasted from the generated code of LALRPOP.
//TODO: We could avoid having those pesky `'ast` lifetimes at the top-level of every trait using
//generic associated types, but it's not entirely trivial - to investigate.
trait LalrpopParser<'ast, T> {
    fn parse<'input, 'err, 'wcard, __TOKEN, __TOKENS>(
        &self,
        alloc: &'ast AstAlloc,
        src_id: FileId,
        errors: &'err mut Vec<
            lalrpop_util::ErrorRecovery<usize, lexer::Token<'input>, self::error::ParseOrLexError>,
        >,
        next_wildcard_id: &'wcard mut usize,
        __tokens0: __TOKENS,
    ) -> Result<
        T,
        lalrpop_util::ParseError<usize, lexer::Token<'input>, self::error::ParseOrLexError>,
    >
    where
        __TOKEN: __ToTriple<'input, 'ast, 'err, 'wcard>,
        __TOKENS: IntoIterator<Item = __TOKEN>;
}

/// Generate boiler-plate code to implement the trait [`LalrpopParser`] for a parser generated by
/// LALRPOP.
macro_rules! generate_lalrpop_parser_impl {
    ($parser:ty, $output:ty) => {
        impl<'ast> LalrpopParser<'ast, $output> for $parser {
            fn parse<'input, 'err, 'wcard, __TOKEN, __TOKENS>(
                &self,
                alloc: &'ast AstAlloc,
                src_id: FileId,
                errors: &'err mut Vec<
                    lalrpop_util::ErrorRecovery<
                        usize,
                        lexer::Token<'input>,
                        self::error::ParseOrLexError,
                    >,
                >,
                next_wildcard_id: &'wcard mut usize,
                __tokens0: __TOKENS,
            ) -> Result<
                $output,
                lalrpop_util::ParseError<usize, lexer::Token<'input>, self::error::ParseOrLexError>,
            >
            where
                __TOKEN: __ToTriple<'input, 'ast, 'err, 'wcard>,
                __TOKENS: IntoIterator<Item = __TOKEN>,
            {
                Self::parse(self, alloc, src_id, errors, next_wildcard_id, __tokens0)
            }
        }
    };
}

generate_lalrpop_parser_impl!(grammar::ExtendedTermParser, ExtendedTerm<Ast<'ast>>);
generate_lalrpop_parser_impl!(grammar::TermParser, Ast<'ast>);
generate_lalrpop_parser_impl!(grammar::FixedTypeParser, Type<'ast>);
generate_lalrpop_parser_impl!(grammar::StaticFieldPathParser, Vec<LocIdent>);
generate_lalrpop_parser_impl!(
    grammar::CliFieldAssignmentParser,
    (Vec<LocIdent>, Ast<'ast>, RawSpan)
);

/// General interface of the various specialized Nickel parsers.
///
/// `T` is the product of the parser (a term, a type, etc.).
pub trait ErrorTolerantParser<'ast, T> {
    /// Parse a value from a lexer with the given `file_id` in an error-tolerant way. This methods
    /// can still fail for non-recoverable errors.
    fn parse_tolerant<'input>(
        &self,
        alloc: &'ast AstAlloc,
        file_id: FileId,
        lexer: impl Iterator<Item = Result<lexer::SpannedToken<'input>, error::LexicalError>>,
    ) -> Result<(T, ParseErrors), ParseError>;

    /// Parse a value from a lexer with the given `file_id`, failing at the first encountered
    /// error.
    fn parse_strict<'input>(
        &self,
        alloc: &'ast AstAlloc,
        file_id: FileId,
        lexer: impl Iterator<Item = Result<lexer::SpannedToken<'input>, error::LexicalError>>,
    ) -> Result<T, ParseErrors>;
}

impl<'ast, T, P> ErrorTolerantParser<'ast, T> for P
where
    P: LalrpopParser<'ast, T>,
{
    fn parse_tolerant<'input>(
        &self,
        alloc: &'ast AstAlloc,
        file_id: FileId,
        lexer: impl Iterator<Item = Result<lexer::SpannedToken<'input>, error::LexicalError>>,
    ) -> Result<(T, ParseErrors), ParseError> {
        let mut parse_errors = Vec::new();
        let mut next_wildcard_id = 0;
        let result = self
            .parse(
                alloc,
                file_id,
                &mut parse_errors,
                &mut next_wildcard_id,
                lexer.map(|x| x.map_err(error::ParseOrLexError::from)),
            )
            .map_err(|err| ParseError::from_lalrpop(err, file_id));

        let parse_errors = ParseErrors::from_recoverable(parse_errors, file_id);
        match result {
            Ok(t) => Ok((t, parse_errors)),
            Err(e) => Err(e),
        }
    }

    fn parse_strict<'input>(
        &self,
        alloc: &'ast AstAlloc,
        file_id: FileId,
        lexer: impl Iterator<Item = Result<lexer::SpannedToken<'input>, error::LexicalError>>,
    ) -> Result<T, ParseErrors> {
        match self.parse_tolerant(alloc, file_id, lexer) {
            Ok((t, e)) if e.no_errors() => Ok(t),
            Ok((_, e)) => Err(e),
            Err(e) => Err(e.into()),
        }
    }
}

/// Additional capabilities for parsers that return `Asts`, offering an error-tolerant interface
/// that is actually infallible.
///
/// The interface of error tolerant parsers is a bit strange: albeit dubbed as error-tolerant,
/// [ErrorTolerantParser::parse_tolerant] is still fallible with the same error type that is
/// returned in the `Ok` case. There are thus some parse errors that are fatal: the issue is that
/// LALRPOP can't generate a proper AST when it can't even get to complete one parsing rule, in
/// which case it bails out. But this is artificial, because we can still produce an AST with one
/// node spanning the full file, this node being the fatal error.
///
/// This is precisely what does [FullyErrorTolerantParser], which wraps [ErrorTolerantParser] when
/// `T` is `Ast<'ast>`.
pub trait FullyErrorTolerantParser<'ast, T> {
    /// Parse a value from a lexer with the given `file_id` in an error-tolerant way.
    ///
    /// When the parser fails without being able to produce a proper AST, we need to construct the
    /// root as the error node. Since this isn't easy to reverse-engineer the whole span of the
    /// original file from the lexer, we take it as an explicit argument.
    fn parse_fully_tolerant<'input>(
        &self,
        alloc: &'ast AstAlloc,
        file_id: FileId,
        lexer: impl Iterator<Item = Result<lexer::SpannedToken<'input>, error::LexicalError>>,
        full_span: RawSpan,
    ) -> (T, ParseErrors);
}

impl<'ast, P> FullyErrorTolerantParser<'ast, Ast<'ast>> for P
where
    P: ErrorTolerantParser<'ast, Ast<'ast>>,
{
    fn parse_fully_tolerant<'input>(
        &self,
        alloc: &'ast AstAlloc,
        file_id: FileId,
        lexer: impl Iterator<Item = Result<lexer::SpannedToken<'input>, error::LexicalError>>,
        full_span: RawSpan,
    ) -> (Ast<'ast>, ParseErrors) {
        match self.parse_tolerant(alloc, file_id, lexer) {
            Ok((ast, e)) => (ast, e),
            Err(e) => {
                let ast = alloc.parse_error(e.clone()).spanned(full_span.into());
                (ast, e.into())
            }
        }
    }
}
