mod expr;
mod filter;
mod helpers;
mod node;

use std::borrow::Cow;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::str;
use std::sync::Arc;

use parser::node::{Call, Macro, Whitespace};
use parser::{CharLit, Expr, FloatKind, IntKind, Num, StrLit, WithSpan};
use proc_macro2::{Span, TokenStream};
use quote::{ToTokens, quote_spanned};
use syn::Token;

use crate::generator::helpers::{clean_path, diff_paths};
use crate::heritage::{Context, Heritage};
use crate::html::write_escaped_str;
use crate::input::{Source, TemplateInput};
use crate::integration::{Buffer, impl_everything, write_header};
use crate::{CompileError, FileInfo, HashMap, SizeHint, field_new, quote_into};

pub(crate) fn template_to_string(
    buf: &mut Buffer,
    input: &TemplateInput<'_>,
    contexts: &HashMap<&Arc<Path>, Context<'_>>,
    heritage: Option<&Heritage<'_, '_>>,
    tmpl_kind: TmplKind<'_>,
) -> Result<SizeHint, CompileError> {
    let generator = Generator::new(
        input,
        contexts,
        heritage,
        MapChain::default(),
        input.block.is_some(),
        BlockInfo::new(),
    );
    let size_hint = match generator.impl_template(buf, tmpl_kind) {
        Err(mut err) if err.span.is_none() => {
            err.span = Some(input.source_span.config_span());
            Err(err)
        }
        result => result,
    }?;

    if tmpl_kind == TmplKind::Struct {
        impl_everything(input.ast, buf);
    }
    Ok(size_hint)
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TmplKind<'a> {
    /// [`askama::Template`]
    Struct,
    /// [`askama::helpers::EnumVariantTemplate`]
    Variant,
    /// Used in `blocks` implementation
    Block(&'a str),
}

/// This enum allows to know if we render the "first phase" of an `extends`, (the `Extends` variant)
/// meaning generating all variables and method/function calls and nothing else. The second one
/// (`Template`) is the "default", we generate everything.
#[derive(Default, Clone, Copy, PartialEq)]
enum RenderFor {
    #[default]
    Template,
    Extends,
}

#[derive(Clone, Copy)]
struct BlockInfo {
    block_name: &'static str,
    level: usize,
}

impl BlockInfo {
    fn new() -> Self {
        Self {
            block_name: "",
            level: 0,
        }
    }

    // FIXME: Instead of this error-prone API, we should use something relying on `Drop` to
    // decrement, or use a callback which would decrement on exit.
    fn increase(&mut self, block_name: &'static str) {
        if self.level == 0 {
            self.block_name = block_name;
        }
        self.level += 1;
    }

    fn decrease(&mut self) {
        self.level -= 1;
    }
}

struct Generator<'a, 'h> {
    /// The template input state: original struct AST and attributes
    input: &'a TemplateInput<'a>,
    /// All contexts, keyed by the package-relative template path
    contexts: &'a HashMap<&'a Arc<Path>, Context<'a>>,
    /// The heritage contains references to blocks and their ancestry
    heritage: Option<&'h Heritage<'a, 'h>>,
    /// Variables accessible directly from the current scope (not redirected to context)
    locals: MapChain<'a>,
    /// Suffix whitespace from the previous literal. Will be flushed to the
    /// output buffer unless suppressed by whitespace suppression on the next
    /// non-literal.
    next_ws: Option<WithSpan<&'a str>>,
    /// Whitespace suppression from the previous non-literal. Will be used to
    /// determine whether to flush prefix whitespace from the next literal.
    skip_ws: Whitespace,
    /// If currently in a block, this will contain the name of a potential parent block
    super_block: Option<(&'a str, usize)>,
    /// Buffer for writable
    buf_writable: WritableBuffer<'a>,
    /// Used in blocks to check if we are inside a filter/let block.
    is_in_block: BlockInfo,
    /// Set of called macros we are currently in. Used to prevent (indirect) recursions.
    seen_callers: Vec<(&'a Macro<'a>, Option<FileInfo<'a>>)>,
    /// The directory path of the calling file.
    caller_dir: CallerDir,
}

enum CallerDir {
    Valid(PathBuf),
    Invalid,
    Unresolved,
}

impl<'a, 'h> Generator<'a, 'h> {
    fn new(
        input: &'a TemplateInput<'a>,
        contexts: &'a HashMap<&'a Arc<Path>, Context<'a>>,
        heritage: Option<&'h Heritage<'a, 'h>>,
        locals: MapChain<'a>,
        buf_writable_discard: bool,
        is_in_block: BlockInfo,
    ) -> Self {
        Self {
            input,
            contexts,
            heritage,
            locals,
            next_ws: None,
            skip_ws: Whitespace::Preserve,
            super_block: None,
            buf_writable: WritableBuffer {
                discard: buf_writable_discard,
                ..Default::default()
            },
            is_in_block,
            seen_callers: Vec::new(),
            caller_dir: CallerDir::Unresolved,
        }
    }

    fn rel_path<'p>(&mut self, path: &'p Path) -> Cow<'p, Path> {
        self.caller_dir()
            .and_then(|caller_dir| {
                diff_paths(path, caller_dir, std::env::var("CARGO_MANIFEST_DIR").ok())
            })
            .map_or(Cow::Borrowed(path), Cow::Owned)
    }

    fn caller_dir(&mut self) -> Option<&Path> {
        match self.caller_dir {
            CallerDir::Valid(ref caller_dir) => return Some(caller_dir.as_path()),
            CallerDir::Invalid => return None,
            CallerDir::Unresolved => {}
        }

        if proc_macro::is_available()
            && let Some(mut local_file) = proc_macro::Span::call_site().local_file()
        {
            local_file.pop();
            if !local_file.is_absolute() {
                local_file = Path::new(".").join(local_file);
            }

            self.caller_dir = CallerDir::Valid(clean_path(&local_file));
            match &self.caller_dir {
                CallerDir::Valid(caller_dir) => Some(caller_dir.as_path()),
                _ => None, // unreachable
            }
        } else {
            self.caller_dir = CallerDir::Invalid;
            None
        }
    }

    // Implement `Template` for the given context struct.
    fn impl_template(
        mut self,
        buf: &mut Buffer,
        tmpl_kind: TmplKind<'a>,
    ) -> Result<SizeHint, CompileError> {
        let ctx = &self.contexts[&self.input.path];

        let span = Span::call_site();
        let target = match tmpl_kind {
            TmplKind::Struct => quote_spanned!(span=> askama::Template),
            TmplKind::Variant => quote_spanned!(span=> askama::helpers::EnumVariantTemplate),
            TmplKind::Block(trait_name) => field_new(trait_name, span),
        };

        let mut paths_ts = TokenStream::new();

        if let Some(full_config_path) = &self.input.config.full_config_path {
            let full_config_path = self.rel_path(full_config_path).display().to_string();
            paths_ts.extend(quote_spanned!(span =>
                const _: &[askama::helpers::core::primitive::u8] =
                    askama::helpers::core::include_bytes!(#full_config_path);
            ));
        }

        // Make sure the compiler understands that the generated code depends on the template files.
        let mut paths = self
            .contexts
            .iter()
            .map(|(path, _ctx)| {
                (
                    &***path,
                    #[cfg(not(feature = "external-sources"))]
                    (),
                    #[cfg(feature = "external-sources")]
                    _ctx,
                )
            })
            .filter(|&(path, _)| {
                // Skip the fake path of templates defined in rust source.
                match self.input.source {
                    #[cfg(feature = "external-sources")]
                    Source::Path(_) => true,
                    Source::Source(_) => *path != *self.input.path,
                }
            })
            .collect::<Vec<_>>();
        paths.sort_by_key(|&(path, _)| path);
        for (path, _ctx) in paths {
            let path = self.rel_path(path).display().to_string();
            paths_ts.extend(quote_spanned!(span=>
                const _: &[askama::helpers::core::primitive::u8] =
                    askama::helpers::core::include_bytes!(#path);
            ));

            #[cfg(all(feature = "external-sources", feature = "nightly-spans"))]
            _ctx.resolve_path(&path);
        }

        let mut content = Buffer::new();
        let size_hint = self.impl_template_inner(ctx, &mut content)?;
        let content = content.into_token_stream();

        let mut size_hint_s = TokenStream::new();
        if tmpl_kind == TmplKind::Struct {
            size_hint_s = quote_spanned!(span=>
                const SIZE_HINT: askama::helpers::core::primitive::usize = #size_hint;
            );
        }

        write_header(self.input.ast, buf, target);
        let var_writer = crate::var_writer();
        let var_values = crate::var_values();
        quote_into!(buf, span, { {
            fn render_into_with_values(
                &self,
                #var_writer: &mut dyn askama::helpers::core::fmt::Write,
                #var_values: &dyn askama::Values,
            ) -> askama::Result<()> {
                #[allow(unused_imports)]
                use askama::{
                    filters::{AutoEscape as _, WriteWritable as _},
                    helpers::{ResultConverter as _, core::fmt::Write as _},
                };

                #paths_ts
                #content
                askama::Result::Ok(())
            }
            #size_hint_s
        } });

        for block in self.input.blocks {
            self.impl_block(buf, block)?;
        }

        Ok(size_hint)
    }

    fn impl_block(
        &self,
        buf: &mut Buffer,
        block: &crate::input::Block,
    ) -> Result<(), CompileError> {
        // RATIONALE: `*self` must be the input type, implementation details should not leak:
        // - impl Self { fn as_block(self) } ->
        // - struct __Askama__Self__as__block__Wrapper { this: self } ->
        // - impl Template for __Askama__Self__as__block__Wrapper { fn render_into_with_values() } ->
        // - impl __Askama__Self__as__block for Self { render_into_with_values() }

        use syn::{GenericParam, Ident, Lifetime, LifetimeParam, Token};

        let span = Span::call_site();
        let ident = &self.input.ast.ident;

        let doc = format!(
            "A sub-template that renders only the block `{}` of [`{ident}`].",
            block.name
        );
        let method_name = format!("as_{}", block.name);
        let trait_name = format!("__Askama__{ident}__as__{}", block.name);
        let wrapper_name = format!("__Askama__{ident}__as__{}__Wrapper", block.name);
        let self_lt_name = format!("'__Askama__{ident}__as__{}__self", block.name);

        let method_id = Ident::new(&method_name, span);
        let trait_id = Ident::new(&trait_name, span);
        let wrapper_id = Ident::new(&wrapper_name, span);
        let self_lt = Lifetime::new(&self_lt_name, span);

        // generics of the input with an additional lifetime to capture `self`
        let mut wrapper_generics = self.input.ast.generics.clone();
        if wrapper_generics.lt_token.is_none() {
            wrapper_generics.lt_token = Some(Token![<](span));
            wrapper_generics.gt_token = Some(Token![>](span));
        }
        wrapper_generics.params.insert(
            0,
            GenericParam::Lifetime(LifetimeParam::new(self_lt.clone())),
        );

        let (impl_generics, ty_generics, where_clause) = self.input.ast.generics.split_for_impl();
        let (wrapper_impl_generics, wrapper_ty_generics, wrapper_where_clause) =
            wrapper_generics.split_for_impl();

        let input = TemplateInput {
            block: Some((&block.name, span)),
            blocks: &[],
            ..self.input.clone()
        };
        let mut template_buf = Buffer::new();
        let size_hint = template_to_string(
            &mut template_buf,
            &input,
            self.contexts,
            self.heritage,
            TmplKind::Block(&trait_name),
        )?;

        let template_buf = template_buf.into_token_stream();
        quote_into!(buf, span, {
            #[allow(missing_docs, non_camel_case_types, non_snake_case, unreachable_pub)]
            const _: () = {
                #template_buf

                pub trait #trait_id {
                    fn render_into_with_values(
                        &self,
                        writer: &mut dyn askama::helpers::core::fmt::Write,
                        values: &dyn askama::Values,
                    ) -> askama::Result<()>;
                }

                impl #impl_generics #ident #ty_generics #where_clause {
                    #[inline]
                    #[doc = #doc]
                    pub fn #method_id(&self) -> impl askama::Template + '_ {
                        #wrapper_id {
                            this: self,
                        }
                    }
                }

                #[askama::helpers::core::prelude::rust_2021::derive(
                    askama::helpers::core::prelude::rust_2021::Clone,
                    askama::helpers::core::prelude::rust_2021::Copy
                )]
                pub struct #wrapper_id #wrapper_generics #wrapper_where_clause {
                    this: &#self_lt #ident #ty_generics,
                }

                impl #wrapper_impl_generics askama::Template
                for #wrapper_id #wrapper_ty_generics #wrapper_where_clause {
                    #[inline]
                    fn render_into_with_values(
                        &self,
                        writer: &mut dyn askama::helpers::core::fmt::Write,
                        values: &dyn askama::Values
                    ) -> askama::Result<()> {
                        <_ as #trait_id>::render_into_with_values(self.this, writer, values)
                    }

                    const SIZE_HINT: askama::helpers::core::primitive::usize = #size_hint;
                }

                // cannot use `crate::integrations::impl_fast_writable()` w/o cloning the struct
                impl #wrapper_impl_generics askama::FastWritable
                for #wrapper_id #wrapper_ty_generics #wrapper_where_clause {
                    #[inline]
                    fn write_into(
                        &self,
                        dest: &mut dyn askama::helpers::core::fmt::Write,
                        values: &dyn askama::Values,
                    ) -> askama::Result<()> {
                        <_ as askama::Template>::render_into_with_values(self, dest, values)
                    }
                }

                // cannot use `crate::integrations::impl_display()` w/o cloning the struct
                impl #wrapper_impl_generics askama::helpers::core::fmt::Display
                for #wrapper_id #wrapper_ty_generics #wrapper_where_clause {
                    #[inline]
                    fn fmt(
                        &self,
                        f: &mut askama::helpers::core::fmt::Formatter<'_>
                    ) -> askama::helpers::core::fmt::Result {
                        <_ as askama::Template>::render_into(self, f)
                            .map_err(|_| askama::helpers::core::fmt::Error)
                    }
                }
            };
        });

        Ok(())
    }

    fn is_var_defined(&self, var_name: &str) -> bool {
        self.locals.get_any(var_name).is_some() || self.input.fields.iter().any(|f| f == var_name)
    }

    /// Like [`is_var_defined()`], but not true for a forward declaration `{% let var %}`.
    fn is_var_assigned(&self, var_name: &str) -> bool {
        if let Some(meta) = self.locals.get(var_name) {
            meta.initialized
        } else {
            self.input.fields.iter().any(|f| f == var_name)
        }
    }
}

#[cfg(target_pointer_width = "16")]
type TargetIsize = i16;
#[cfg(target_pointer_width = "32")]
type TargetIsize = i32;
#[cfg(target_pointer_width = "64")]
type TargetIsize = i64;

#[cfg(target_pointer_width = "16")]
type TargetUsize = u16;
#[cfg(target_pointer_width = "32")]
type TargetUsize = u32;
#[cfg(target_pointer_width = "64")]
type TargetUsize = u64;

#[cfg(not(any(
    target_pointer_width = "16",
    target_pointer_width = "32",
    target_pointer_width = "64"
)))]
const _: () = {
    panic!("unknown cfg!(target_pointer_width)");
};

/// In here, we inspect in the expression if it is a literal, and if it is, whether it
/// can be escaped at compile time.
fn compile_time_escape<'a>(expr: &WithSpan<Box<Expr<'a>>>, escaper: &str) -> Option<Writable<'a>> {
    // we only optimize for known escapers
    enum OutputKind {
        Html,
        Text,
    }

    // we only optimize for known escapers
    let output = match escaper.strip_prefix("askama::filters::")? {
        "Html" => OutputKind::Html,
        "Text" => OutputKind::Text,
        _ => return None,
    };

    // for now, we only escape strings, chars, numbers, and bools at compile time
    let value = match ***expr {
        Expr::StrLit(StrLit {
            prefix: None,
            content,
            ..
        }) => {
            if content.find('\\').is_none() {
                // if the literal does not contain any backslashes, then it does not need unescaping
                Cow::Borrowed(content)
            } else {
                // the input could be string escaped if it contains any backslashes
                let input = format!(r#""{content}""#);
                let input = input.parse().ok()?;
                let input = syn::parse2::<syn::LitStr>(input).ok()?;
                Cow::Owned(input.value())
            }
        }
        Expr::CharLit(CharLit {
            prefix: None,
            content,
        }) => {
            if content.find('\\').is_none() {
                // if the literal does not contain any backslashes, then it does not need unescaping
                Cow::Borrowed(content)
            } else {
                // the input could be string escaped if it contains any backslashes
                let input = format!(r#"'{content}'"#);
                let input = input.parse().ok()?;
                let input = syn::parse2::<syn::LitChar>(input).ok()?;
                Cow::Owned(input.value().to_string())
            }
        }
        Expr::NumLit(_, value) => {
            enum NumKind {
                Int(Option<IntKind>),
                Float(Option<FloatKind>),
            }

            let (orig_value, kind) = match value {
                Num::Int(value, kind) => (value, NumKind::Int(kind)),
                Num::Float(value, kind) => (value, NumKind::Float(kind)),
            };
            let value = match orig_value.chars().any(|c| c == '_') {
                true => Cow::Owned(orig_value.chars().filter(|&c| c != '_').collect()),
                false => Cow::Borrowed(orig_value),
            };

            fn int<T: ToString, E>(
                from_str_radix: impl Fn(&str, u32) -> Result<T, E>,
                value: &str,
            ) -> Option<String> {
                let mut chars = value.chars();
                let (value, radix) = if let Some('0') = chars.next() {
                    match chars.next() {
                        Some('x') => (&value[2..], 16),
                        Some('o') => (&value[2..], 8),
                        Some('b') => (&value[2..], 2),
                        Some(_) | None => (value, 10),
                    }
                } else {
                    (value, 10)
                };

                Some(from_str_radix(value, radix).ok()?.to_string())
            }

            let value = match kind {
                NumKind::Int(Some(IntKind::I8)) => int(i8::from_str_radix, &value)?,
                NumKind::Int(Some(IntKind::I16)) => int(i16::from_str_radix, &value)?,
                NumKind::Int(Some(IntKind::I32)) => int(i32::from_str_radix, &value)?,
                NumKind::Int(Some(IntKind::I64)) => int(i64::from_str_radix, &value)?,
                NumKind::Int(Some(IntKind::I128)) => int(i128::from_str_radix, &value)?,
                NumKind::Int(Some(IntKind::Isize)) => int(TargetIsize::from_str_radix, &value)?,
                NumKind::Int(Some(IntKind::U8)) => int(u8::from_str_radix, &value)?,
                NumKind::Int(Some(IntKind::U16)) => int(u16::from_str_radix, &value)?,
                NumKind::Int(Some(IntKind::U32)) => int(u32::from_str_radix, &value)?,
                NumKind::Int(Some(IntKind::U64)) => int(u64::from_str_radix, &value)?,
                NumKind::Int(Some(IntKind::U128)) => int(u128::from_str_radix, &value)?,
                NumKind::Int(Some(IntKind::Usize)) => int(TargetUsize::from_str_radix, &value)?,
                NumKind::Int(None) => match value.starts_with('-') {
                    true => int(i128::from_str_radix, &value)?,
                    false => int(u128::from_str_radix, &value)?,
                },
                NumKind::Float(Some(FloatKind::F32)) => value.parse::<f32>().ok()?.to_string(),
                NumKind::Float(Some(FloatKind::F64) | None) => {
                    value.parse::<f64>().ok()?.to_string()
                }
                // FIXME: implement once `f16` and `f128` are available
                NumKind::Float(Some(FloatKind::F16 | FloatKind::F128)) => return None,
            };
            match value == orig_value {
                true => Cow::Borrowed(orig_value),
                false => Cow::Owned(value),
            }
        }
        Expr::BoolLit(true) => Cow::Borrowed("true"),
        Expr::BoolLit(false) => Cow::Borrowed("false"),
        _ => return None,
    };

    // escape the un-string-escaped input using the selected escaper
    Some(Writable::Lit(match output {
        OutputKind::Text => WithSpan::new(value, expr.span()),
        OutputKind::Html => {
            let mut escaped = String::with_capacity(value.len() + 20);
            write_escaped_str(&mut escaped, &value).ok()?;
            match escaped == value {
                true => WithSpan::new(value, expr.span()),
                false => WithSpan::new(Cow::Owned(escaped), expr.span()),
            }
        }
    }))
}

#[derive(Clone, Default, Debug)]
struct LocalVariableMeta {
    refs: Option<String>,
    initialized: bool,
}

#[derive(Clone)]
struct LocalCallerMeta<'a> {
    def: &'a Call<'a>,
    call_ctx: Context<'a>,
}

impl core::fmt::Debug for LocalCallerMeta<'_> {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("LocalCallerMeta")
            .field("def", &self.def)
            .finish()
    }
}

#[derive(Clone, Debug)]
enum LocalMeta<'a> {
    /// Normal variable
    Variable(LocalVariableMeta),

    /// This special variable is a caller alias. It's another name for caller().
    CallerAlias(LocalCallerMeta<'a>),

    /// Represents a "negative" local variable. Meaning: When the resolve methods
    /// encounters a negative on its path down the stack of scopes, it will immediately
    /// return without result. This is required to "block out" variables outside of a certain scope
    Negative,
}

impl<'a> LocalMeta<'a> {
    /// Variable declaration only - no value yet.
    const fn var_decl() -> Self {
        Self::Variable(LocalVariableMeta {
            refs: None,
            initialized: false,
        })
    }

    /// Variable definition - fully initialized.
    const fn var_def() -> Self {
        Self::Variable(LocalVariableMeta {
            refs: None,
            initialized: true,
        })
    }

    /// Variable referencing another
    const fn var_with_ref(refs: String) -> Self {
        Self::Variable(LocalVariableMeta {
            refs: Some(refs),
            initialized: true,
        })
    }

    /// Special variable aliasing a `caller()`
    const fn caller(def: &'a Call<'a>, call_ctx: Context<'a>) -> Self {
        Self::CallerAlias(LocalCallerMeta { def, call_ctx })
    }
}

#[derive(Debug)]
struct MapChain<'a> {
    scopes: Vec<HashMap<Cow<'a, str>, LocalMeta<'a>>>,
}

impl<'a> MapChain<'a> {
    fn new_empty() -> Self {
        Self { scopes: vec![] }
    }

    /// Iterates the scopes in reverse and searches for a local variable (of any kind) with the given key.
    ///
    /// # Returns
    /// - `Some(LocalMeta)` if any kind of local entry (except negative) with the key was found.
    /// - `None` otherwise
    fn get_any<'b>(&'b self, key: &str) -> Option<&'b LocalMeta<'a>> {
        match self.scopes.iter().rev().find_map(|set| set.get(key)) {
            Some(LocalMeta::Negative) => None,
            Some(local) => Some(local),
            _ => None,
        }
    }

    /// Iterates the scopes in reverse and searches for a local variable with the given key.
    ///
    /// # Returns
    /// - `Some(LocalVariableMeta)` if the first encountered entry for key was a variable
    /// - `None` otherwise
    fn get<'b>(&'b self, key: &str) -> Option<&'b LocalVariableMeta> {
        match self.scopes.iter().rev().find_map(|set| set.get(key)) {
            Some(LocalMeta::Variable(var)) => Some(var),
            _ => None,
        }
    }

    /// Iterates the scopes in reverse and searches for a `CallerAlias`
    ///
    /// # Returns
    /// - `Some(LocalCallerMeta)` if the first encountered entry for key was a caller alias
    /// - `None` otherwise
    fn get_caller<'b>(&'b self, key: &str) -> Option<&'b LocalCallerMeta<'a>> {
        match self.scopes.iter().rev().find_map(|set| set.get(key)) {
            Some(LocalMeta::CallerAlias(caller)) => Some(caller),
            _ => None,
        }
    }

    fn is_current_empty(&self) -> bool {
        self.scopes.last().unwrap().is_empty()
    }

    fn insert(&mut self, key: Cow<'a, str>, val: LocalMeta<'a>) {
        self.scopes.last_mut().unwrap().insert(key, val);

        // Note that if `insert` returns `Some` then it implies
        // an identifier is reused. For e.g. `{% macro f(a, a) %}`
        // and `{% let (a, a) = ... %}` then this results in a
        // generated template, which when compiled fails with the
        // compile error "identifier `a` used more than once".
    }

    fn insert_with_default(&mut self, key: Cow<'a, str>) {
        self.insert(key, LocalMeta::var_decl());
    }

    fn resolve(&self, name: &str) -> Option<String> {
        self.get(&Cow::Borrowed(name)).map(|meta| match &meta.refs {
            Some(expr) => expr.clone(),
            None => name.to_string(),
        })
    }

    fn resolve_or_self(&self, name: &str) -> String {
        self.resolve(name).unwrap_or_else(|| format!("self.{name}"))
    }

    fn stack_push(&mut self) {
        self.scopes.push(HashMap::default());
    }

    fn stack_pop(&mut self) {
        self.scopes.pop().unwrap();
    }
}

impl Default for MapChain<'_> {
    fn default() -> Self {
        Self {
            scopes: vec![HashMap::default()],
        }
    }
}

/// Returns `true` if enough assumptions can be made,
/// to determine that `self` is copyable.
fn is_copyable(expr: &Expr<'_>) -> bool {
    is_copyable_within_op(expr, false)
}

fn is_copyable_within_op(expr: &Expr<'_>, within_op: bool) -> bool {
    match expr {
        Expr::BoolLit(_)
        | Expr::NumLit(_, _)
        | Expr::StrLit(_)
        | Expr::CharLit(_)
        | Expr::BinOp(_)
        | Expr::Range(..) => true,
        Expr::Unary(.., expr) => is_copyable_within_op(expr, true),
        Expr::NamedArgument(_, expr) => is_copyable_within_op(expr, true),
        // The result of a call likely doesn't need to be borrowed,
        // as in that case the call is more likely to return a
        // reference in the first place then.
        Expr::Call { .. } | Expr::Path(..) | Expr::Filter(..) | Expr::RustMacro(..) => true,
        // If the `expr` is within a `Unary` or `BinOp` then
        // an assumption can be made that the operand is copy.
        // If not, then the value is moved and adding `.clone()`
        // will solve that issue. However, if the operand is
        // implicitly borrowed, then it's likely not even possible
        // to get the template to compile.
        _ => within_op && is_associated_item_self(expr),
    }
}

/// Returns `true` if this is an `AssociatedItem` where the `obj` is `"self"`.
fn is_associated_item_self(mut expr: &Expr<'_>) -> bool {
    loop {
        match expr {
            Expr::AssociatedItem(obj, _) if matches!(***obj, Expr::Var("self")) => return true,
            Expr::AssociatedItem(obj, _) if matches!(***obj, Expr::AssociatedItem(..)) => {
                expr = obj
            }
            _ => return false,
        }
    }
}

#[derive(Clone, Copy, Debug)]
enum DisplayWrap {
    Wrapped,
    Unwrapped,
}

#[derive(Default, Debug)]
struct WritableBuffer<'a> {
    buf: Vec<Writable<'a>>,
    discard: bool,
}

impl<'a> WritableBuffer<'a> {
    fn push(&mut self, writable: Writable<'a>) {
        if !self.discard {
            self.buf.push(writable);
        }
    }
}

impl<'a> Deref for WritableBuffer<'a> {
    type Target = [Writable<'a>];

    #[inline]
    fn deref(&self) -> &Self::Target {
        self.buf.as_slice()
    }
}

#[derive(Debug)]
enum Writable<'a> {
    Lit(WithSpan<Cow<'a, str>>),
    Expr(&'a WithSpan<Box<Expr<'a>>>),
}

macro_rules! make_token_match {
    ($op:ident @ $span:ident => $($tt:tt)+) => {
        match $op {
            $(stringify!($tt) => Token![$tt]($span).into_token_stream(),)+
            _ => unreachable!(),
        }
    };
}

#[inline]
#[track_caller]
fn logic_op(op: &str, span: proc_macro2::Span) -> TokenStream {
    make_token_match!(op @ span => && || ^)
}

#[inline]
#[track_caller]
fn unary_op(op: &str, span: proc_macro2::Span) -> TokenStream {
    make_token_match!(op @ span => - ! * &)
}

#[inline]
#[track_caller]
fn range_op(op: &str, span: proc_macro2::Span) -> TokenStream {
    make_token_match!(op @ span => .. ..=)
}

#[inline]
#[track_caller]
fn binary_op(op: &str, span: proc_macro2::Span) -> TokenStream {
    make_token_match!(
        op @ span =>
        * / % + - << >> & ^ | == != < > <= >= && || .. ..=
        = += -= *= /= %= &= |= ^= <<= >>=
    )
}
