use crate::bytecode::ast::{
    compat::{FromAst, ToMainline},
    typ::Type,
    Ast, AstAlloc,
};
use crate::error::{ParseError, ParseErrors};
use crate::files::FileId;
use crate::identifier::LocIdent;
use crate::metrics;
use crate::position::RawSpan;
use lalrpop_util::lalrpop_mod;

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

use grammar::__ToTriple;

pub mod error;
pub mod lexer;
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::ParseError>,
        >,
        next_wildcard_id: &'wcard mut usize,
        __tokens0: __TOKENS,
    ) -> Result<T, lalrpop_util::ParseError<usize, lexer::Token<'input>, self::error::ParseError>>
    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::ParseError,
                    >,
                >,
                next_wildcard_id: &'wcard mut usize,
                __tokens0: __TOKENS,
            ) -> Result<
                $output,
                lalrpop_util::ParseError<usize, lexer::Token<'input>, self::error::ParseError>,
            >
            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(
        &self,
        alloc: &'ast AstAlloc,
        file_id: FileId,
        lexer: lexer::Lexer,
    ) -> Result<(T, ParseErrors), ParseError>;

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

impl<'ast, T, P> ErrorTolerantParser<'ast, T> for P
where
    P: LalrpopParser<'ast, T>,
{
    fn parse_tolerant(
        &self,
        alloc: &'ast AstAlloc,
        file_id: FileId,
        lexer: lexer::Lexer,
    ) -> 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_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(
        &self,
        alloc: &'ast AstAlloc,
        file_id: FileId,
        lexer: lexer::Lexer,
    ) -> 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()),
        }
    }
}

/// General interface of the various specialized Nickel parsers.
///
/// This trait is a compatibility layer version of [ErrorTolerantParser]. It produces data of the
/// old, mainline types because the current pipeline still depends on them (defined in
/// [crate::term]). Eventually we'll get rid of it and only use [ErrorTolerantParser], which
/// produces the new AST instead.
pub trait ErrorTolerantParserCompat<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_compat(
        &self,
        file_id: FileId,
        lexer: lexer::Lexer,
    ) -> Result<(T, ParseErrors), ParseError>;

    /// Parse a value from a lexer with the given `file_id`, failing at the first encountered
    /// error.
    fn parse_strict_compat(&self, file_id: FileId, lexer: lexer::Lexer) -> Result<T, ParseErrors>;
}

impl<'ast> FromAst<ExtendedTerm<Ast<'ast>>> for ExtendedTerm<crate::term::RichTerm> {
    fn from_ast(ast: &ExtendedTerm<Ast<'ast>>) -> Self {
        match ast {
            ExtendedTerm::Term(t) => ExtendedTerm::Term(t.to_mainline()),
            ExtendedTerm::ToplevelLet(ident, t) => {
                ExtendedTerm::ToplevelLet(*ident, t.to_mainline())
            }
        }
    }
}

// Generate boilerplate impl to produce legacy mainline types from the available parsers.
macro_rules! generate_compat_impl {
    ($parser:ty, $output:ty) => {
        impl ErrorTolerantParserCompat<$output> for $parser {
            fn parse_tolerant_compat(
                &self,
                file_id: FileId,
                lexer: lexer::Lexer,
            ) -> Result<($output, ParseErrors), ParseError> {
                let alloc = AstAlloc::new();
                self.parse_tolerant(&alloc, file_id, lexer).map(|(t, e)| {
                    (
                        metrics::measure_runtime!("runtime:ast_conversion", t.to_mainline()),
                        e,
                    )
                })
            }

            fn parse_strict_compat(
                &self,
                file_id: FileId,
                lexer: lexer::Lexer,
            ) -> Result<$output, ParseErrors> {
                let alloc = AstAlloc::new();
                self.parse_strict(&alloc, file_id, lexer)
                    .map(|t| metrics::measure_runtime!("runtime:ast_conversion", t.to_mainline()))
            }
        }
    };
}

generate_compat_impl!(
    grammar::ExtendedTermParser,
    ExtendedTerm<crate::term::RichTerm>
);
generate_compat_impl!(grammar::TermParser, crate::term::RichTerm);
generate_compat_impl!(grammar::FixedTypeParser, crate::typ::Type);

impl ErrorTolerantParserCompat<(Vec<LocIdent>, crate::term::RichTerm, RawSpan)>
    for grammar::CliFieldAssignmentParser
{
    fn parse_tolerant_compat(
        &self,
        file_id: FileId,
        lexer: lexer::Lexer,
    ) -> Result<((Vec<LocIdent>, crate::term::RichTerm, RawSpan), ParseErrors), ParseError> {
        self.parse_tolerant(&AstAlloc::new(), file_id, lexer)
            .map(|((path, term, span), e)| ((path, term.to_mainline(), span), e))
    }

    fn parse_strict_compat(
        &self,
        file_id: FileId,
        lexer: lexer::Lexer,
    ) -> Result<(Vec<LocIdent>, crate::term::RichTerm, RawSpan), ParseErrors> {
        self.parse_strict(&AstAlloc::new(), file_id, lexer)
            .map(|(path, term, span)| (path, term.to_mainline(), span))
    }
}

// This implementation doesn't do any conversion, but hide away the (useless, in this case)
// [crate::bytecode::ast::AstAlloc] parameter.
impl ErrorTolerantParserCompat<Vec<LocIdent>> for grammar::StaticFieldPathParser {
    fn parse_tolerant_compat(
        &self,
        file_id: FileId,
        lexer: lexer::Lexer,
    ) -> Result<(Vec<LocIdent>, ParseErrors), ParseError> {
        self.parse_tolerant(&AstAlloc::new(), file_id, lexer)
    }

    fn parse_strict_compat(
        &self,
        file_id: FileId,
        lexer: lexer::Lexer,
    ) -> Result<Vec<LocIdent>, ParseErrors> {
        self.parse_strict(&AstAlloc::new(), file_id, lexer)
    }
}
