#![warn(clippy::pedantic)]
#![warn(missing_docs)]
#![doc = include_str!("../README.md")]

use ariadne::{Cache, Source};
use ast::{SourceFile, Template};
use builtins::{StandardContextWorker, StandardContextualizer, StandardEscaper};
use itertools::Itertools;
use maybe_sync::{dyn_maybe_send_sync, Rc};
#[cfg(feature = "serde")]
use rendertime::value::serde_values;
use std::fmt::{Debug, Display};
use std::hash::Hash;
use std::marker::PhantomData;
use std::{collections::HashMap, fmt};

#[doc(hidden)]
#[allow(non_snake_case)]
pub mod __internal__tests_util;

mod ast;

mod parse;
pub use parse::Error as ParseError;

pub(crate) mod rendertime;

pub use rendertime::Error as RendertimeError;
pub use rendertime::Value;

#[cfg(feature = "serde")]
pub use serde_values::Error as SerializationError;

mod type_checking;
pub use type_checking::{Error as TypeCheckingError, Indentation, Type};

pub mod builtins;

#[derive(Debug, Clone, PartialEq, Eq)]
/// An error that can happen when static text gets contextualized.
pub enum ContextualizationError {
    /// When the contextualizer does not know about a text type
    UnknownTextType(String),
    /// When the static text is invalid in the given context
    InvalidStatic,
    /// When a certain text type cannot be safely included in the given context
    IncompatibleTextType {
        /// The current context
        target_ty: String,
        /// The text type that cannot be included in that context
        source_ty: String,
    },
    /// When a template or template literal ends in a different context than it started in.
    StartAndEndDifferentContext(String, String),
}

impl Display for ContextualizationError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::UnknownTextType(ty) => write!(f, "Text type {ty:?} unknown"),
            Self::InvalidStatic => write!(f, "Static text invalid for given text type"),
            Self::IncompatibleTextType {
                target_ty,
                source_ty: dynamic_ty,
            } => write!(
                f,
                "The target type {target_ty:?} is incompatible with the source type {dynamic_ty:?}"
            ),
            Self::StartAndEndDifferentContext(start, end) => write!(
                f,
                "The static text starts in type {start:?} but ends in {end:?}"
            ),
        }
    }
}

impl std::error::Error for ContextualizationError {}

/// A trait implemented by users wishing to change the way text types behave. This determines
/// the default text type, how static text get's analyzed and changes the context and how to
/// escape text to safely get from one text type to another.
///
/// The actual contextualizing is done by the implementations [`ContextWorker`].
///
/// While the escaping is done by an [`Escaper`] of the implementations choosing.
pub trait Contextualizer<Esc, Wrk>: Debug
where
    Esc: Escaper,
    Wrk: ContextWorker<Esc>,
{
    /// This determines the text type that parameters get when their type is omitted. It is usually
    /// `text`, but implementors are free to change that.
    fn default_text_type(&self) -> String;

    /// This returns an appropriate [`ContextWorker`] for the given text type.
    ///
    /// # Errors
    ///
    /// In case of an unknown text type, this returns an [`ContextualizationError::UnknownTextType`].
    fn contextualize(&self, text_ty: &str) -> Result<Wrk, ContextualizationError>;

    /// This returns an [`Escaper`] to safely go from the `source_ty` text type to the `target_ty` text type.
    ///
    /// It is a shortcut to `self.contextualize(target_ty)?.dynamic(source_ty)`.
    ///
    /// # Errors
    ///
    /// If either the source or target text types are unknown this will return an
    /// [`ContextualizationError::UnknownTextType`].
    ///
    /// If the two types are incompatible this will return [`ContextualizationError::IncompatibleTextType`].
    fn escaper(&self, target_ty: &str, source_ty: &str) -> Result<Esc, ContextualizationError> {
        self.contextualize(target_ty)?.dynamic(source_ty)
    }
}

/// A trait to be implemented in tandem with [`Contextualizer`] by users wishing to change how
/// static text is analysed to determine context and appropriate text type.
pub trait ContextWorker<Esc: Escaper> {
    /// Push additional static text to potentially change the current context.
    ///
    /// # Errors
    ///
    /// The static text may be invalid in the current context in which case a
    /// [`ContextualizationError::InvalidStatic`] error gets returned.
    fn push_static(&mut self, s: &str) -> Result<(), ContextualizationError>;

    /// Calling this implies that the template wants to include a dynamic piece of text here. Given
    /// the text type of this text this shall return an appropriate [`Escaper`] to safely do so.
    ///
    /// # Errors
    ///
    /// In case of incompatible text types this returns [`ContextualizationError::IncompatibleTextType`].
    fn dynamic(&mut self, input_ty: &str) -> Result<Esc, ContextualizationError>;
}

/// A trait to be implemented in tandem with [`Contextualizer`] by users wishing to change how
/// values are escaped to make it safe to include in a different context.
pub trait Escaper: Debug + Clone {
    /// The core functionality. Given a [`Value`] (usually a [`Value::Text`]) this shall return a
    /// [`Value`] that is safe to include in the context this [`Escaper`] was derived from.
    ///
    /// # Errors
    ///
    /// In case the given [`Value`] cannot be included safely implementations are permitted to
    /// instead return a [`String`] describing why that is.
    fn escape(&self, inp: Value) -> Result<Value, String>;
}

/// A function to be used by template authors. There are several builtin ones. This can be implemented by users wishing to extend (or change) those.
pub trait Function {
    /// This determines whether a given set of parameter [`Type`]s are accepted by the [`Function`]
    /// at type check time and if so which [`Type`] will be returned by the [`Function`] in that case.
    /// To not do so when [`Function::run`] is called at render time is a logic error and might
    /// result in unpredictable outcomes.
    fn accepts(&self, args: Vec<Type>) -> Option<Type>;

    /// To be called at render time by the engine. Consumes the provided arguments and shall
    /// produce a value given those.
    ///
    /// # Errors
    ///
    /// Implementators are permitted to instead return a [`Box`]ed [`std::error::Error`] in case of
    /// failure which will terminate the rendering and surface the error to the caller.
    fn run(
        &self,
        args: Vec<Value>,
    ) -> Result<Value, Box<maybe_sync::dyn_maybe_send_sync!(std::error::Error)>>;
}

/// The core struct of the templating engine. It represents fully type checked and ready to
/// [`Templates::render`] templates.
///
/// It can be obtained several ways:
/// - Using the `include_templates!` macro
/// - Using [`Templates::new`]
/// - Building them via a [`TemplatesBuilder`] obtained by either [`Templates::builder`] or [`TemplatesBuilder::default`].
pub struct Templates<Esc = StandardEscaper> {
    /// not just a map, since the indexes get referenced as template calls
    tmpl_names: HashMap<String, usize>,
    tmpls: Vec<type_checking::Template<Esc>>,
}

impl<Esc> Debug for Templates<Esc> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Templates")
            .field("templates", &self.tmpl_names.keys().collect_vec())
            .finish_non_exhaustive()
    }
}

/// A span referencing a portion of template source code. Typically returned by errors to indicate
/// the particular location of the problem.
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
pub struct Span<'s> {
    start: usize,
    end: usize,
    path_and_source: (&'s str, &'s str),
}

impl Debug for Span<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{start}..{end}", start = self.start, end = self.end)
    }
}

impl<'s> chumsky::span::Span for Span<'s> {
    type Context = (&'s str, &'s str);

    type Offset = usize;

    fn new(context: Self::Context, range: std::ops::Range<Self::Offset>) -> Self {
        Self {
            start: range.start,
            end: range.end,
            path_and_source: context,
        }
    }

    fn context(&self) -> Self::Context {
        self.path_and_source
    }

    fn start(&self) -> Self::Offset {
        self.start
    }

    fn end(&self) -> Self::Offset {
        self.end
    }
}

impl<'s> ariadne::Span for Span<'s> {
    type SourceId = (&'s str, &'s str);

    fn source(&self) -> &Self::SourceId {
        &self.path_and_source
    }

    fn start(&self) -> usize {
        self.start
    }

    fn end(&self) -> usize {
        self.end
    }
}

/// The error type returned if building [`Templates`] failed.
pub enum Error<'s> {
    /// If the parsing of one or more source files failed
    Parse(ParseError<'s>),
    /// If the final type checking at the end failed
    TypeChecking(Box<type_checking::Error<'s>>),
}

struct AriadneCache<'s>(HashMap<&'s str, Source<&'s str>>);

impl<'s> Cache<(&'s str, &'s str)> for AriadneCache<'s> {
    type Storage = &'s str;

    fn fetch(
        &mut self,
        id: &(&'s str, &'s str),
    ) -> Result<&Source<Self::Storage>, impl fmt::Debug> {
        Ok::<_, Box<dyn Debug>>(self.0.entry(id.0).or_insert_with(|| Source::from(id.1)))
    }

    fn display<'a>(&self, id: &'a (&'s str, &'s str)) -> Option<impl fmt::Display + 'a> {
        Some(Box::new(id.0))
    }
}

impl Debug for Error<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("\n")?;
        <Self as Display>::fmt(self, f)
    }
}

impl Display for Error<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Error::Parse(err) => {
                if f.alternate() {
                    write!(f, "{err:#}")
                } else {
                    write!(f, "{err}")
                }
            }
            Error::TypeChecking(err) => {
                if f.alternate() {
                    write!(f, "{err:#}")
                } else {
                    write!(f, "{err}")
                }
            }
        }
    }
}

impl std::error::Error for Error<'_> {}

/// The main way of obtaining [`Templates`].
///
/// Created via [`TemplatesBuilder::default`] or [`TemplatesBuilder::new_with_ctxer`] if a specific
/// Contextualizer is desired.
///
/// Add source files and functions then [`TemplatesBuilder::build`] when ready.
pub struct TemplatesBuilder<
    's,
    Ctx = StandardContextualizer,
    Esc = StandardEscaper,
    Wrk = StandardContextWorker,
> {
    tmpls: Vec<Template<'s>>,
    funcs: HashMap<&'s str, Rc<dyn_maybe_send_sync!(Function)>>,
    ctxer: Ctx,
    _esc: PhantomData<Esc>,
    _wrk: PhantomData<Wrk>,
}

impl Debug for TemplatesBuilder<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("TemplatesBuilder")
            .field("tmpls", &self.tmpls)
            .field("funcs", &self.funcs.keys())
            .finish()
    }
}

impl<'s, Ctx, Esc, Wrk> TemplatesBuilder<'s, Ctx, Esc, Wrk> {
    /// Adds a source file with path and content.
    ///
    /// # Errors
    /// Returns [`ParseError`] if the content could not be parsed.
    pub fn with_source_file(
        mut self,
        path: &'s str,
        source: &'s str,
    ) -> Result<TemplatesBuilder<'s, Ctx, Esc, Wrk>, ParseError<'s>> {
        self.tmpls
            .extend(SourceFile::parse(path, source)?.into_templates());

        Ok(self)
    }

    /// Adds multiple source files with paths and contents.
    ///
    /// # Errors
    /// Returns [`ParseError`] if any of the contents could not be parsed.
    pub fn with_source_files<I: IntoIterator<Item = (&'s str, &'s str)>>(
        self,
        sources: I,
    ) -> Result<TemplatesBuilder<'s, Ctx, Esc, Wrk>, ParseError<'s>> {
        let mut res = Ok(self);

        for (path, source) in sources {
            let inter_res = SourceFile::parse(path, source).map(SourceFile::into_templates);

            res = match (res, inter_res) {
                (Ok(mut builder), Ok(source)) => {
                    builder.tmpls.extend(source);
                    Ok(builder)
                }
                (Ok(_), Err(errors)) => Err(errors),
                (res @ Err(_), Ok(_)) => res,
                (Err(mut prev_error), Err(next_error)) => {
                    if let ParseError::Multiple(ref mut prev) = prev_error {
                        prev.push(next_error);
                        return Err(prev_error);
                    }

                    return Err(ParseError::Multiple(vec![prev_error, next_error]));
                }
            }
        }

        res
    }

    /// Adds a specific function to make it available to the templates. Can be done before / after
    /// / in the middle of adding the source files.
    ///
    /// Requires a name and the implementation.
    #[must_use]
    pub fn with_function(
        mut self,
        name: &'s str,
        func: impl Function + 'static + maybe_sync::MaybeSync + maybe_sync::MaybeSend,
    ) -> TemplatesBuilder<'s, Ctx, Esc, Wrk> {
        self.funcs.insert(name, Rc::new(func));

        self
    }

    /// Adds multiple functions to make them avaiable to the templates. Can also be done at any
    /// point compared to adding the templates.
    ///
    /// Requires a name and implementation for each.
    #[must_use]
    pub fn with_functions(
        mut self,
        funcs: impl IntoIterator<Item = (&'s str, Rc<dyn_maybe_send_sync!(Function)>)>,
    ) -> TemplatesBuilder<'s, Ctx, Esc, Wrk> {
        self.funcs.extend(funcs);

        self
    }

    /// After adding all source files and functions this actually attempts to build the templates.
    /// This involves type checking the templates and preparing them for rendering.
    ///
    /// The final result is a [`Templates`] ready for rendering.
    ///
    /// # Errors
    ///
    /// Since type checking can fail so can this. In which case it returns [`Error::TypeChecking`].
    pub fn build(self) -> Result<Templates<Esc>, Error<'s>>
    where
        Esc: Escaper,
        Ctx: Contextualizer<Esc, Wrk>,
        Wrk: ContextWorker<Esc>,
    {
        let (tmpls, tmpl_names) = type_checking::type_check_templates(
            self.tmpls,
            // important: builtin functions first, so they get overwritten by user defined
            // functions
            builtins::functions()
                .into_iter()
                .chain(self.funcs.into_iter())
                .collect(),
            &self.ctxer,
        )
        .map_err(Box::new)
        .map_err(Error::TypeChecking)?;

        Ok(Templates { tmpl_names, tmpls })
    }

    /// Returns an iterator of the names all previously registered templates.
    ///
    /// Intended for debugging.
    #[must_use]
    pub fn template_names<'a>(&'a self) -> impl ExactSizeIterator<Item = &'s str> + 'a {
        self.tmpls.iter().map(|tmpl| tmpl.name.0)
    }
}

impl<Ctx: Default, Esc, Wrk> TemplatesBuilder<'_, Ctx, Esc, Wrk> {
    /// Creates a new [`TemplatesBuilder`], optionally with different Contextualization pipeline as
    /// long as the [`Contextualizer`] implements [`Default`]. If you need a [`Contextualizer`]
    /// which does not, you can use [`TemplatesBuilder::new_with_ctxer`] to provide one.
    #[must_use]
    pub fn new() -> Self {
        TemplatesBuilder {
            tmpls: Vec::new(),
            funcs: builtins::functions().into_iter().collect(),
            ctxer: Ctx::default(),
            _esc: PhantomData,
            _wrk: PhantomData,
        }
    }
}

impl<Ctx, Esc, Wrk> TemplatesBuilder<'_, Ctx, Esc, Wrk> {
    /// Creates a [`TemplatesBuilder`] with a specific [`Contextualizer`].
    #[must_use]
    pub fn new_with_ctxer(ctxer: Ctx) -> Self {
        TemplatesBuilder {
            tmpls: Vec::new(),
            funcs: builtins::functions().into_iter().collect(),
            ctxer,
            _esc: PhantomData,
            _wrk: PhantomData,
        }
    }
}

impl<Ctx> Default for TemplatesBuilder<'_, Ctx>
where
    Ctx: Default,
{
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(feature = "serde")]
/// The error type that gets returned in case a render using a serde argument fails.
#[derive(Debug)]
pub enum SerdeRenderError {
    /// If the serialization failed
    Serde(serde_values::Error),
    /// If the rendering itself failed
    Rendertime(rendertime::Error),
}

#[cfg(feature = "serde")]
impl Display for SerdeRenderError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Serde(err) => Display::fmt(&err, f),
            Self::Rendertime(err) => Display::fmt(&err, f),
        }
    }
}

#[cfg(feature = "serde")]
impl std::error::Error for SerdeRenderError {}

impl<'s, Esc: Escaper> Templates<Esc> {
    /// Directly creates a [`Templates`] providing source files and functions.
    ///
    /// # Errors
    ///
    /// Returns an [`Error`] in case parsing or type checking fails.
    pub fn new<'f, Ctx, Wrk>(
        source_files: impl IntoIterator<Item = (&'s str, &'s str)>,
        functions: HashMap<&'s str, Rc<dyn_maybe_send_sync!(Function)>>,
    ) -> Result<Templates<Esc>, Error<'s>>
    where
        Ctx: Contextualizer<Esc, Wrk> + Default,
        Esc: Escaper,
        Wrk: ContextWorker<Esc>,
    {
        TemplatesBuilder::<Ctx, Esc, Wrk>::new()
            .with_source_files(source_files)
            .map_err(Error::Parse)?
            .with_functions(functions)
            .build()
    }

    /// Creates a new [`TemplatesBuilder`] to build a [`Templates`].
    #[must_use]
    pub fn builder<Ctx, Wrk>() -> TemplatesBuilder<'s, Esc, Ctx, Wrk>
    where
        Esc: Default,
    {
        TemplatesBuilder::new()
    }

    /// Directly creates a [`Templates`] providing source files and functions as well as a specific
    /// [`Contextualizer`].
    ///
    /// # Errors
    ///
    /// Returns an [`Error`] in case parsing or type checking fails.
    pub fn new_with_ctxer<'f, Ctx, Wrk>(
        source_files: impl IntoIterator<Item = (&'s str, &'s str)>,
        functions: HashMap<&'s str, Rc<dyn_maybe_send_sync!(Function)>>,
        ctxer: Ctx,
    ) -> Result<Templates<Esc>, Error<'s>>
    where
        Ctx: Contextualizer<Esc, Wrk> + Default,
        Esc: Escaper,
        Wrk: ContextWorker<Esc>,
    {
        TemplatesBuilder::<Ctx, Esc, Wrk>::new_with_ctxer(ctxer)
            .with_source_files(source_files)
            .map_err(Error::Parse)?
            .with_functions(functions)
            .build()
    }

    /// Renders the specified template using the specified arguments, running the template and the
    /// functions it invokes. The argument needs to be a [`Value::Struct`] containing the parameters.
    /// Returns the output of the template as a [`String`].
    ///
    /// # Errors
    ///
    /// Templates can fail to render, such as when a wrong value was provided
    /// ([`RendertimeError::WrongParameters`] or [`RendertimeError::NonStructRootValue`]), an escaper
    /// reported an error ([`RendertimeError::Escape`]) or a function did so ([`RendertimeError::Function`]).
    pub fn render<V: Into<Value>>(&self, name: &str, args: V) -> Result<String, RendertimeError> {
        let Value::Struct(args) = args.into() else {
            return Err(rendertime::Error::NonStructRootValue);
        };
        rendertime::tmpl_output(self, self.tmpl_names[name], args)
    }

    /// Renders the specified template using the specified arguments, running the template and the
    /// functions it invokes. The argument needs to be a [`Value::Struct`] containing the parameters.
    /// Writes the output of the template into a user provided [`fmt::Write`] implementation.
    ///
    /// # Errors
    ///
    /// Templates can fail to render, such as when a wrong value was provided
    /// ([`RendertimeError::WrongParameters`] or [`RendertimeError::NonStructRootValue`]),
    /// the [`fmt::Write`] instance failed to write ([`RendertimeError::Fmt`]),
    /// an escaper reported an error ([`RendertimeError::Escape`])
    /// or a function did so ([`RendertimeError::Function`]).
    pub fn render_fmt<V: Into<Value>>(
        &self,
        name: &str,
        args: V,
        writer: impl fmt::Write,
    ) -> Result<(), rendertime::Error> {
        let Value::Struct(args) = args.into() else {
            return Err(rendertime::Error::NonStructRootValue);
        };
        rendertime::write_tmpl(writer, self, self.tmpl_names[name], args)
    }

    /// Renders the specified template using the given serializable argument.
    ///
    /// See [`Templates::render`] for more.
    ///
    /// # Errors
    ///
    /// Can fail like [`Templates::render`] in the [`SerdeRenderError::Rendertime`] case,
    /// but can also fail if the serialization failed ([`SerdeRenderError::Serde`],
    /// [`serde_values::Error`]).
    #[cfg(feature = "serde")]
    pub fn render_serde(
        &self,
        name: &str,
        args: impl serde::ser::Serialize,
    ) -> Result<String, SerdeRenderError> {
        self.render(
            name,
            args.serialize(serde_values::ValueSerializer)
                .map_err(SerdeRenderError::Serde)?,
        )
        .map_err(SerdeRenderError::Rendertime)
    }

    #[cfg(feature = "serde")]
    /// Renders the specified template using the given serializable argument into the user
    /// specified [`fmt::Write`] implementation.
    ///
    /// See [`Templates::render_serde`] and [`Templates::render_fmt`] for more.
    ///
    /// # Errors
    ///
    /// Can fail like [`Templates::render_serde`] in the [`SerdeRenderError::Serde`] case,
    /// but can also fail like [`Templates::render_fmt`] in the [`SerdeRenderError::Rendertime`] case.
    pub fn render_serde_fmt(
        &self,
        name: &str,
        args: impl serde::ser::Serialize,
        writer: impl fmt::Write,
    ) -> Result<(), SerdeRenderError> {
        self.render_fmt(
            name,
            args.serialize(serde_values::ValueSerializer)
                .map_err(SerdeRenderError::Serde)?,
            writer,
        )
        .map_err(SerdeRenderError::Rendertime)
    }
}
