// Lowering pipeline macro
//
//  Copyright (C) 2014-2023 Ryan Specialty, LLC.
//
//  This file is part of TAME.
//
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  (at your option) any later version.
//
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with this program.  If not, see <http://www.gnu.org/licenses/>.

//! Declarative lowering pipelines.
//!
//! This module defines the `lower_pipeline` macro,
//!   which allows for declarative lowering pipeline definitions.
//! For more information of what a lowering pipeline is,
//!   and to see TAMER's pipelines,
//!   see the [parent module](super).

#[cfg(doc)]
use crate::parse::ParseState;

/// Generate an error sum type for a lowering pipeline.
///
/// Given a series of [`ParseState`] types,
///   this derives a sum type capable of representing the associated
///   [`ParseState::Error`] of each.
/// See the [parent module](super) for more information,
///   including the challenges/concerns with this approach.
/// In particular,
///   note that all lifetimes on the [`ParseState`] type are rewritten to be
///   `'static';
///     all associated `Error` types must not contain non-static lifetimes,
///       as is the standard convention in TAMER.
macro_rules! lower_error_sum {
    (
        $(#[$meta:meta])*
        $vis:vis $name:ident = $(
            $st:ident $(<$($l:lifetime,)* $($c:literal,)* $($t:ident,)*>)?
        )+
    ) => {
        // Pair `'static` with each lifetime so that it may be used to
        //   replace the respective lifetime in `@gen`
        //     (we need an iteration token).
        lower_error_sum!(
            @gen
            $(#[$meta])*
            $vis $name = $($st$(<$($l: 'static,)* $($c,)* $($t,)*>)?)+
        );
    };

    (
        @gen
        $(#[$meta:meta])*
        $vis:vis $name:ident = $(
            $st:ident $(<
                $($_:lifetime: $l:lifetime,)*
                //             ^^ `'static` (see above)
                $($c:literal,)*
                $($t:ident,)*
            >)?
        )+
    ) => {
        $(#[$meta])*
        #[derive(Debug, PartialEq)]
        $vis enum $name<ES: Diagnostic + PartialEq + 'static> {
            Src(ParseError<UnknownToken, ES>),
            $(
                $st(ParseStateError<$st$(<$($l,)* $($c,)* $($t),* >)?>)
                //                          ^^ `'static`
            ),+
        }

        impl<ES: Diagnostic + PartialEq + 'static> From<ParseError<UnknownToken, ES>>
            for $name<ES>
        {
            fn from(e: ParseError<UnknownToken, ES>) -> Self {
                Self::Src(e)
            }
        }

        $(
            impl<ES: Diagnostic + PartialEq + 'static>
                From<ParseStateError<$st$(<$($l,)* $($c,)* $($t),*>)?>>
                for $name<ES>
            {
                fn from(e: ParseStateError<$st$(<$($l,)* $($c,)* $($t),*>)?>) -> Self {
                    Self::$st(e)
                }
            }
        )+

        impl<ES: Diagnostic + PartialEq + 'static> std::fmt::Display for $name<ES> {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                match self {
                    Self::Src(e) => std::fmt::Display::fmt(e, f),
                    $(
                        Self::$st(e) => std::fmt::Display::fmt(e, f),
                    )+
                }
            }
        }

        impl<ES: Diagnostic + PartialEq + 'static> Diagnostic for $name<ES> {
            fn describe(&self) -> Vec<crate::diagnose::AnnotatedSpan> {
                match self {
                    Self::Src(e) => e.describe(),
                    $(
                        Self::$st(e) => e.describe(),
                    )+
                }
            }
        }
    };
}

/// Declaratively define a lowering pipeline.
///
/// A lowering pipeline stitches together parsers such that the objects of
///   one become the tokens of the next;
///     see the [module-level documentation](self) for more information.
///
/// Composing those types results in a significant amount of boilerplate.
/// This macro is responsible for generating a function that,
///   given a source and optional [`ParseState`](crate::parse::ParseState) contexts,
///   will carry out the lowering operation and invoke the provided sink on
///     each object that comes out the end.
///
/// TODO: This is not yet finalized;
///   more documentation is needed once the approach is solid.
macro_rules! lower_pipeline {
    ($(
        $(#[$meta:meta])*
        $vis:vis $fn:ident$(<$l:lifetime>)? -> $struct:ident
            $(|>
              $lower_name:tt$(<$($lower_t:tt),+>)?
              $([$ctx:ident])?
              $(, until ($until:pat))?
            )*
            ;
    )*) => {$(
        paste::paste! {
            lower_error_sum! {
                /// Recoverable error for
                #[doc=concat!("[`", stringify!($fn), "`]")]
                /// lowering pipeline.
                ///
                /// This represents an error that occurred from one of the
                ///   [`ParseState`]s in the lowering pipeline.
                /// Since all [`ParseState`]s are expected to attempt
                ///   recovery on failure,
                ///     this error represents a _recoverable_ error.
                /// Whether or not the error should be treated as
                ///   recoverable is entirely at the discretion of the sink
                ///   provided to the pipeline;
                ///     a sink may choose to promote all errors to
                ///     unrecoverable.
                $vis [<$struct Error>] = $(
                    $lower_name$(<$($lower_t,)+>)?
                )*
            }
        }

        lower_pipeline!(
            @pipeline
            $(#[$meta])*
            $vis $fn$(<$l>)? -> $struct
                $(|> $lower_name$(<$($lower_t),+>)? $([$ctx])? $(, until ($until))?)*
        );
    )*};

    (@pipeline
        $(#[$meta:meta])*
        $vis:vis $fn:ident$(<$l:lifetime>)? -> $struct:ident
            $(|> $lower:ty $([$ctx:ident])? $(, until ($until:pat))?)*
    ) => {paste::paste!{
        $(#[$meta])*
        ///
        /// Pipeline Definition
        /// ===================
        /// This pipeline consists of the following parsers,
        ///   where the first parser accepts the `src` token stream and the
        ///   final parser outputs to the provided `sink`.
        ///
        $(
            #[doc=concat!("  - [`", stringify!($lower), "`]")]
            $(
                #[doc=concat!(" with context `", stringify!($ctx), "`")]
            )?
            $(
                #[doc=concat!(
                    "up to and including the first token matching `",
                    stringify!($until),
                    "`",
                )]
            )?
            #[doc="\n"]
        )*
        ///
        /// The type signature of this function is complex due to the wiring
        ///   of the types of all parsers in the pipeline.
        /// It can be understood as:
        ///
        ///   1. A function accepting _context_ for whatever parsers request
        ///        it,
        ///          allowing both for configuration and for state to
        ///          persist between separate pipelines.
        ///      This returns a closure representing a configured pipeline.
        ///   2. The _source_ token stream is accepted by the closure,
        ///         which consists of tokens expected by the first parser
        ///         in the pipeline;
        ///   3. A _sink_ serves as the final destination for the token
        ///        stream.
        ///   4. A [`Result`] consisting of the updated context that was
        ///        originally passed into the function,
        ///          so that it may be utilized in future pipelines.
        ///   5. A _recoverable error_ type
        #[doc=concat!("[`", stringify!([<$struct Error>]), "`]")]
        ///      that may be utilized when
        ///        compilation should continue despite an error.
        ///      See [`crate::pipeline`] for more information.
        ///   6. An _unrecoverable error_ type `EU` that may be yielded by
        ///        the sink to terminate compilation immediately.
        ///      This is a component of the [`Result`] type that is
        ///        ultimately yielded as the result of this function.
        $vis fn $fn<$($l,)? ES: Diagnostic + 'static, EU: Diagnostic, SA, SB>(
            $(
                // Each parser may optionally receive context from an
                //   earlier run.
                $($ctx: impl Into<<$lower as ParseState>::PubContext>,)?
            )*
        ) -> impl FnOnce(SA, SB) -> Result<
            (
                $(
                    // Any context that is passed in is also returned so
                    //   that individual pipelines can continue to build
                    //   upon state from previous pipelines.
                    $( lower_pipeline!(@ret_ctx_ty $lower, $ctx), )?
                )*
            ),
            EU
        >
        where
            // Unrecoverable errors (EU) are errors that the sink chooses
            //   not to handle.
            // It is constructed explicitly from the sink,
            //   so the only type conversion that we are concerned about
            //   ourselves is producing it from a finalization error,
            //     which is _not_ an error that parsers are expected to
            //     recover from.
            EU: From<FinalizeError>,

            SA: LowerSource<
                UnknownToken,
                lower_pipeline!(@first_tok_ty $($lower),*),
                ES
            >,

            SB: FnMut(
                Result<lower_pipeline!(@last_obj_ty $($lower),*), [<$struct Error>]<ES>>
            ) -> Result<(), EU>
        {
            move |src, sink| {
                // Recoverable error type (for brevity).
                #[doc(hidden)]
                type ER<T> = [<$struct Error>]<T>;

                let lower_pipeline!(@ret_pat $($($ctx)?)*) = lower_pipeline!(
                    @body_head(src, sink)
                    $((|> $lower $([$ctx])? $(, until ($until))?))*
                )?;

                Ok(($(
                    $($ctx,)?
                )*))
            }
        }
    }};

    (@ret_ctx_ty $lower:ty, $_ctx:ident) => {
        <$lower as ParseState>::PubContext
    };

    // We are able to determine the necessary type of the source token
    //   stream by observing what token is expected by the first parser in the
    //   pipeline.
    (@first_tok_ty $lower:ty, $($rest:ty),+) => {
        <$lower as ParseState>::Token
    };

    // The last object type enters the sink.
    (@last_obj_ty $lower:ty, $($rest:ty),+) => {
        lower_pipeline!(@last_obj_ty $($rest),+)
    };
    (@last_obj_ty $last:ty) => {
        <$last as ParseState>::Object
    };

    // Because of how the lowering pipeline composes,
    //   contexts are nested with a terminal unit at the left,
    //     as in `(((), B), A)` with `A` being a parser that appears earlier
    //     in the pipeline than `B`.
    (@ret_pat $ctx:ident $($rest:tt)*) => {
        (lower_pipeline!(@ret_pat $($rest)*), $ctx)
    };
    (@ret_pat) => {
        ()
    };

    // First lowering operation from the source.
    //
    // This doesn't support context or `until`;
    //   it can be added if ever it is needed.
    (
        @body_head($src:ident, $sink:ident)
        (|> $head:ty) $($rest:tt)*
    ) => {
        Lower::<
            ParsedObject<UnknownToken, _, ES>,
            $head,
            ER<ES>,
        >::lower::<_, EU>(&mut $src.map(|result| result.map_err(ER::Src)), |next| {
            lower_pipeline!(
                @body_inner(next, $head, $sink)
                $($rest)*
            )
        })
    };

    // TODO: Roll this into the above
    (
        @body_head($src:ident, $sink:ident)
        (|> $head:ty [$ctx:ident]) $($rest:tt)*
    ) => {
        Lower::<
            ParsedObject<UnknownToken, _, ES>,
            $head,
            ER<ES>,
        >::lower_with_context::<_, EU>(
            &mut $src.map(|result| result.map_err(ER::Src)),
            $ctx,
            |next| {
                lower_pipeline!(
                    @body_inner(next, $head, $sink)
                    $($rest)*
                )
            }
        )
    };

    // Lower without context
    //   (with the default context for the parser).
    //
    // This doesn't support `until`;
    //   it can be added if needed.
    (
        @body_inner($next:ident, $lower_prev:ty, $sink:ident)
        (|> $lower:ty) $($rest:tt)*
    ) => {
        Lower::<$lower_prev, $lower, _>::lower($next, |next| {
            lower_pipeline!(
                @body_inner(next, $lower, $sink)
                $($rest)*
            )
        })
    };

    // Lower with a context provided by the caller,
    //   optionally with an `until` clause that stops at (and includes) a
    //   matching object from the previous parser.
    //
    // TODO: Roll this into the above
    (
        @body_inner($next:ident, $lower_prev:ty, $sink:ident)
        (|> $lower:ty [$ctx:ident] $(, until ($until:pat))?) $($rest:tt)*
    ) => {{
        // Shadow $next with a new iterator if `until` clause was provided.
        // `take_while` unfortunately doesn't include the first object that
        //   does not match,
        //     thus the more verbose `scan` which includes the first
        //     non-match and then trips the iterator.
        $(
            let $next = &mut $next.scan(false, |st, rtok| match st {
                true => None,
                false => {
                    *st =
                        matches!(rtok, Ok(Parsed::Object($until)));
                    Some(rtok)
                }
            });
        )?

        Lower::<$lower_prev, $lower, _>::lower_with_context($next, $ctx, |next| {
            lower_pipeline!(
                @body_inner(next, $lower, $sink)
                $($rest)*
            )
        })
    }};

    // Terminal sink is applied after there are no more lowering operations
    //   remaining in the pipeline definition.
    (@body_inner($next:ident, $lower_prev:ty, $sink:ident)) => {
        terminal::<$lower_prev, _>($next).try_for_each($sink)
    };
}
