use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher};

use ecow::{EcoString, eco_format};
use indexmap::IndexMap;
use indexmap::map::Entry;
use rustc_hash::FxBuildHasher;
use typst_syntax::Span;

use crate::diag::{HintedStrResult, HintedString, StrResult, WarningSink, bail};
use crate::foundations::{
    Func, IntoValue, NativeElement, NativeFunc, NativeFuncData, NativeType, Value,
};
use crate::{Category, Library};

/// A stack of scopes.
#[derive(Debug, Default, Clone)]
pub struct Scopes<'a> {
    /// The active scope.
    pub top: Scope,
    /// The stack of lower scopes.
    pub scopes: Vec<Scope>,
    /// The standard library.
    pub base: Option<&'a Library>,
}

impl<'a> Scopes<'a> {
    /// Create a new, empty hierarchy of scopes.
    pub fn new(base: Option<&'a Library>) -> Self {
        Self { top: Scope::new(), scopes: vec![], base }
    }

    /// Enter a new scope.
    pub fn enter(&mut self) {
        self.scopes.push(std::mem::take(&mut self.top));
    }

    /// Exit the topmost scope.
    ///
    /// This panics if no scope was entered.
    pub fn exit(&mut self) {
        self.top = self.scopes.pop().expect("no pushed scope");
    }

    /// Try to access a binding immutably.
    pub fn get(&self, var: &str) -> HintedStrResult<&Binding> {
        std::iter::once(&self.top)
            .chain(self.scopes.iter().rev())
            .find_map(|scope| scope.get(var))
            .or_else(|| {
                self.base.and_then(|base| match base.global.scope().get(var) {
                    Some(binding) => Some(binding),
                    None if var == "std" => Some(&base.std),
                    None => None,
                })
            })
            .ok_or_else(|| unknown_variable(var))
    }

    /// Try to access a binding mutably.
    pub fn get_mut(&mut self, var: &str) -> HintedStrResult<&mut Binding> {
        std::iter::once(&mut self.top)
            .chain(&mut self.scopes.iter_mut().rev())
            .find_map(|scope| scope.get_mut(var))
            .ok_or_else(|| {
                match self.base.and_then(|base| base.global.scope().get(var)) {
                    Some(_) => cannot_mutate_constant(var),
                    _ if var == "std" => cannot_mutate_constant(var),
                    _ => unknown_variable(var),
                }
            })
    }

    /// Try to access a binding immutably in math.
    pub fn get_in_math(&self, var: &str) -> HintedStrResult<&Binding> {
        std::iter::once(&self.top)
            .chain(self.scopes.iter().rev())
            .find_map(|scope| scope.get(var))
            .or_else(|| {
                self.base.and_then(|base| match base.math.scope().get(var) {
                    Some(binding) => Some(binding),
                    None if var == "std" => Some(&base.std),
                    None => None,
                })
            })
            .ok_or_else(|| {
                unknown_variable_math(
                    var,
                    self.base.is_some_and(|base| base.global.scope().get(var).is_some()),
                )
            })
    }

    /// Check if an std variable is shadowed.
    pub fn check_std_shadowed(&self, var: &str) -> bool {
        self.base.is_some_and(|base| base.global.scope().get(var).is_some())
            && std::iter::once(&self.top)
                .chain(self.scopes.iter().rev())
                .any(|scope| scope.get(var).is_some())
    }
}

/// A map from binding names to values.
#[derive(Default, Clone)]
pub struct Scope {
    map: IndexMap<EcoString, Binding, FxBuildHasher>,
    deduplicate: bool,
    category: Option<Category>,
}

/// Scope construction.
impl Scope {
    /// Create a new empty scope.
    pub fn new() -> Self {
        Default::default()
    }

    /// Create a new scope with duplication prevention.
    pub fn deduplicating() -> Self {
        Self { deduplicate: true, ..Default::default() }
    }

    /// Enter a new category.
    pub fn start_category(&mut self, category: Category) {
        self.category = Some(category);
    }

    /// Reset the category.
    pub fn reset_category(&mut self) {
        self.category = None;
    }

    /// Define a native function through a Rust type that shadows the function.
    #[track_caller]
    pub fn define_func<T: NativeFunc>(&mut self) -> &mut Binding {
        let data = T::data();
        self.define(data.name, Func::from(data))
    }

    /// Define a native function with raw function data.
    #[track_caller]
    pub fn define_func_with_data(
        &mut self,
        data: &'static NativeFuncData,
    ) -> &mut Binding {
        self.define(data.name, Func::from(data))
    }

    /// Define a native type.
    #[track_caller]
    pub fn define_type<T: NativeType>(&mut self) -> &mut Binding {
        let ty = T::ty();
        self.define(ty.short_name(), ty)
    }

    /// Define a native element.
    #[track_caller]
    pub fn define_elem<T: NativeElement>(&mut self) -> &mut Binding {
        let elem = T::ELEM;
        self.define(elem.name(), elem)
    }

    /// Define a built-in with compile-time known name and returns a mutable
    /// reference to it.
    ///
    /// When the name isn't compile-time known, you should instead use:
    /// - `Vm::bind` if you already have [`Binding`]
    /// - `Vm::define`  if you only have a [`Value`]
    /// - [`Scope::bind`](Self::bind) if you are not operating in the context of
    ///   a `Vm` or if you are binding to something that is not an AST
    ///   identifier (e.g. when constructing a dynamic
    ///   [`Module`](super::Module))
    #[track_caller]
    pub fn define(&mut self, name: &'static str, value: impl IntoValue) -> &mut Binding {
        #[cfg(debug_assertions)]
        if self.deduplicate && self.map.contains_key(name) {
            panic!("duplicate definition: {name}");
        }

        let mut binding = Binding::detached(value);
        binding.category = self.category;
        self.bind(name.into(), binding)
    }
}

/// Scope manipulation and access.
impl Scope {
    /// Inserts a binding into this scope and returns a mutable reference to it.
    ///
    /// Prefer `Vm::bind` if you are operating in the context of a `Vm`.
    pub fn bind(&mut self, name: EcoString, binding: Binding) -> &mut Binding {
        match self.map.entry(name) {
            Entry::Occupied(mut entry) => {
                entry.insert(binding);
                entry.into_mut()
            }
            Entry::Vacant(entry) => entry.insert(binding),
        }
    }

    /// Try to access a binding immutably.
    pub fn get(&self, var: &str) -> Option<&Binding> {
        self.map.get(var)
    }

    /// Try to access a binding mutably.
    pub fn get_mut(&mut self, var: &str) -> Option<&mut Binding> {
        self.map.get_mut(var)
    }

    /// Iterate over all definitions.
    pub fn iter(&self) -> impl Iterator<Item = (&EcoString, &Binding)> {
        self.map.iter()
    }
}

impl Debug for Scope {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        f.write_str("Scope ")?;
        f.debug_map()
            .entries(self.map.iter().map(|(k, v)| (k, v.read())))
            .finish()
    }
}

impl Hash for Scope {
    fn hash<H: Hasher>(&self, state: &mut H) {
        state.write_usize(self.map.len());
        for item in &self.map {
            item.hash(state);
        }
        self.deduplicate.hash(state);
        self.category.hash(state);
    }
}

/// Defines the associated scope of a Rust type.
pub trait NativeScope {
    /// The constructor function for the type, if any.
    fn constructor() -> Option<&'static NativeFuncData>;

    /// Get the associated scope for the type.
    fn scope() -> Scope;
}

/// A bound value with metadata.
#[derive(Debug, Clone, Hash)]
pub struct Binding {
    /// The bound value.
    value: Value,
    /// The kind of binding, determines how the value can be accessed.
    kind: BindingKind,
    /// A span associated with the binding.
    span: Span,
    /// The category of the binding.
    category: Option<Category>,
    /// The deprecation information if this item is deprecated.
    deprecation: Option<Box<Deprecation>>,
}

/// The different kinds of slots.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
enum BindingKind {
    /// A normal, mutable binding.
    Normal,
    /// A captured copy of another variable.
    Captured(Capturer),
}

impl Binding {
    /// Create a new binding with a span marking its definition site.
    pub fn new(value: impl IntoValue, span: Span) -> Self {
        Self {
            value: value.into_value(),
            span,
            kind: BindingKind::Normal,
            category: None,
            deprecation: None,
        }
    }

    /// Create a binding without a span.
    pub fn detached(value: impl IntoValue) -> Self {
        Self::new(value, Span::detached())
    }

    /// Marks this binding as deprecated, with the given `message`.
    pub fn deprecated(&mut self, deprecation: Deprecation) -> &mut Self {
        self.deprecation = Some(Box::new(deprecation));
        self
    }

    /// Read the value.
    pub fn read(&self) -> &Value {
        &self.value
    }

    /// Read the value, checking for deprecation.
    ///
    /// As the `sink`
    /// - pass `()` to ignore the message.
    /// - pass `(&mut engine, span)` to emit a warning into the engine.
    pub fn read_checked(&self, mut sink: impl WarningSink) -> &Value {
        if let Some(message) = &self.deprecation {
            sink.emit((**message).into());
        }
        &self.value
    }

    /// Try to write to the value.
    ///
    /// This fails if the value is a read-only closure capture.
    pub fn write(&mut self) -> StrResult<&mut Value> {
        match self.kind {
            BindingKind::Normal => Ok(&mut self.value),
            BindingKind::Captured(capturer) => bail!(
                "variables from outside the {} are \
                 read-only and cannot be modified",
                match capturer {
                    Capturer::Function => "function",
                    Capturer::Context => "context expression",
                },
            ),
        }
    }

    /// Create a copy of the binding for closure capturing.
    pub fn capture(&self, capturer: Capturer) -> Self {
        Self {
            kind: BindingKind::Captured(capturer),
            ..self.clone()
        }
    }

    /// A span associated with the stored value.
    pub fn span(&self) -> Span {
        self.span
    }

    /// A deprecation message for the value, if any.
    pub fn deprecation(&self) -> Option<&Deprecation> {
        self.deprecation.as_deref()
    }

    /// The category of the value, if any.
    pub fn category(&self) -> Option<Category> {
        self.category
    }
}

/// What the variable was captured by.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Capturer {
    /// Captured by a function / closure.
    Function,
    /// Captured by a context expression.
    Context,
}

/// Information about a deprecated binding.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Deprecation {
    /// A deprecation message for the definition.
    message: &'static str,
    /// A version in which the deprecated binding is planned to be removed.
    until: Option<&'static str>,
}

impl Deprecation {
    /// Creates new deprecation info with a default message to display when
    /// emitting the deprecation warning.
    pub fn new() -> Self {
        Self { message: "item is deprecated", until: None }
    }

    /// Set the message to display when emitting the deprecation warning.
    pub fn with_message(mut self, message: &'static str) -> Self {
        self.message = message;
        self
    }

    /// Set the version in which the binding is planned to be removed.
    pub fn with_until(mut self, version: &'static str) -> Self {
        self.until = Some(version);
        self
    }

    /// The message to display when emitting the deprecation warning.
    pub fn message(&self) -> &'static str {
        self.message
    }

    /// The version in which the binding is planned to be removed.
    pub fn until(&self) -> Option<&'static str> {
        self.until
    }
}

impl Default for Deprecation {
    fn default() -> Self {
        Self::new()
    }
}

impl From<Deprecation> for HintedString {
    fn from(deprecation: Deprecation) -> Self {
        HintedString::new(deprecation.message.into()).with_hints(
            deprecation
                .until
                .map(|v| eco_format!("it will be removed in Typst {}", v)),
        )
    }
}

/// The error message when trying to mutate a variable from the standard
/// library.
#[cold]
fn cannot_mutate_constant(var: &str) -> HintedString {
    eco_format!("cannot mutate a constant: {}", var).into()
}

/// The error message when a variable wasn't found.
#[cold]
fn unknown_variable(var: &str) -> HintedString {
    let mut res = HintedString::new(eco_format!("unknown variable: {}", var));

    if var.contains('-') {
        res.hint(eco_format!(
            "if you meant to use subtraction, \
             try adding spaces around the minus sign{}: `{}`",
            if var.matches('-').count() > 1 { "s" } else { "" },
            var.replace('-', " - ")
        ));
    }

    res
}

/// The error message when a variable wasn't found it math.
#[cold]
fn unknown_variable_math(var: &str, in_global: bool) -> HintedString {
    let mut res = HintedString::new(eco_format!("unknown variable: {}", var));

    if matches!(var, "none" | "auto" | "false" | "true") {
        res.hint(eco_format!(
            "if you meant to use a literal, \
             try adding a hash before it: `#{var}`",
        ));
    } else if in_global {
        res.hint(eco_format!(
            "`{var}` is not available directly in math, but is in the standard library",
        ));
        res.hint(eco_format!(
            "to access `{var}` in code mode you can add a hash: `#{var}`",
        ));
        res.hint(eco_format!(
            "or access `{var}` in math mode by using the `std` module: `std.{var}`",
        ));
    } else {
        res.hint(eco_format!(
            "if you meant to display multiple letters as is, \
             try adding spaces between each letter: `{}`",
            var.chars().flat_map(|c| [' ', c]).skip(1).collect::<EcoString>()
        ));
        res.hint(eco_format!(
            "or if you meant to display this as text, \
             try placing it in quotes: `\"{var}\"`"
        ));
    }

    res
}
