use ecow::eco_format;
use rustc_hash::FxHashSet;
use typst_library::diag::{At, SourceDiagnostic, SourceResult, bail, error};
use typst_library::foundations::{Array, Dict, Value};
use typst_syntax::ast::{self, AstNode};

use crate::{Access, Eval, Vm};

impl Eval for ast::LetBinding<'_> {
    type Output = Value;

    fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
        let value = match self.init() {
            Some(expr) => expr.eval(vm)?,
            None => Value::None,
        };
        if vm.flow.is_some() {
            return Ok(Value::None);
        }

        match self.kind() {
            ast::LetBindingKind::Normal(pattern) => destructure(vm, pattern, value)?,
            ast::LetBindingKind::Closure(ident) => vm.define(ident, value),
        }

        Ok(Value::None)
    }
}

impl Eval for ast::DestructAssignment<'_> {
    type Output = Value;

    fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
        let value = self.value().eval(vm)?;
        destructure_impl(vm, self.pattern(), value, &mut |vm, expr, value| {
            let location = expr.access(vm)?;
            *location = value;
            Ok(())
        })?;
        Ok(Value::None)
    }
}

/// Destructures a value into a pattern.
pub(crate) fn destructure(
    vm: &mut Vm,
    pattern: ast::Pattern,
    value: Value,
) -> SourceResult<()> {
    destructure_impl(vm, pattern, value, &mut |vm, expr, value| match expr {
        ast::Expr::Ident(ident) => {
            vm.define(ident, value);
            Ok(())
        }
        _ => bail!(expr.span(), "cannot assign to this expression"),
    })
}

/// Destruct the given value into the pattern and apply the function to each binding.
fn destructure_impl<F>(
    vm: &mut Vm,
    pattern: ast::Pattern,
    value: Value,
    f: &mut F,
) -> SourceResult<()>
where
    F: Fn(&mut Vm, ast::Expr, Value) -> SourceResult<()>,
{
    match pattern {
        ast::Pattern::Normal(expr) => f(vm, expr, value)?,
        ast::Pattern::Placeholder(_) => {}
        ast::Pattern::Parenthesized(parenthesized) => {
            destructure_impl(vm, parenthesized.pattern(), value, f)?
        }
        ast::Pattern::Destructuring(destruct) => match value {
            Value::Array(value) => destructure_array(vm, destruct, value, f)?,
            Value::Dict(value) => destructure_dict(vm, destruct, value, f)?,
            _ => bail!(pattern.span(), "cannot destructure {}", value.ty()),
        },
    }
    Ok(())
}

fn destructure_array<F>(
    vm: &mut Vm,
    destruct: ast::Destructuring,
    value: Array,
    f: &mut F,
) -> SourceResult<()>
where
    F: Fn(&mut Vm, ast::Expr, Value) -> SourceResult<()>,
{
    let len = value.as_slice().len();
    let mut i = 0;

    for p in destruct.items() {
        match p {
            ast::DestructuringItem::Pattern(pattern) => {
                let Ok(v) = value.at(i as i64, None) else {
                    bail!(wrong_number_of_elements(destruct, len));
                };
                destructure_impl(vm, pattern, v, f)?;
                i += 1;
            }
            ast::DestructuringItem::Spread(spread) => {
                let sink_size = (1 + len).checked_sub(destruct.items().count());
                let sink = sink_size.and_then(|s| value.as_slice().get(i..i + s));
                let (Some(sink_size), Some(sink)) = (sink_size, sink) else {
                    bail!(wrong_number_of_elements(destruct, len));
                };
                if let Some(expr) = spread.sink_expr() {
                    f(vm, expr, Value::Array(sink.into()))?;
                }
                i += sink_size;
            }
            ast::DestructuringItem::Named(named) => {
                bail!(named.span(), "cannot destructure named pattern from an array")
            }
        }
    }

    if i < len {
        bail!(wrong_number_of_elements(destruct, len));
    }

    Ok(())
}

fn destructure_dict<F>(
    vm: &mut Vm,
    destruct: ast::Destructuring,
    dict: Dict,
    f: &mut F,
) -> SourceResult<()>
where
    F: Fn(&mut Vm, ast::Expr, Value) -> SourceResult<()>,
{
    let mut sink = None;
    let mut used = FxHashSet::default();

    for p in destruct.items() {
        match p {
            // Shorthand for a direct identifier.
            ast::DestructuringItem::Pattern(ast::Pattern::Normal(ast::Expr::Ident(
                ident,
            ))) => {
                let v = dict.get(&ident).at(ident.span())?;
                f(vm, ast::Expr::Ident(ident), v.clone())?;
                used.insert(ident.get().clone());
            }
            ast::DestructuringItem::Named(named) => {
                let name = named.name();
                let v = dict.get(&name).at(name.span())?;
                destructure_impl(vm, named.pattern(), v.clone(), f)?;
                used.insert(name.get().clone());
            }
            ast::DestructuringItem::Spread(spread) => sink = spread.sink_expr(),
            ast::DestructuringItem::Pattern(expr) => {
                bail!(expr.span(), "cannot destructure unnamed pattern from dictionary");
            }
        }
    }

    if let Some(expr) = sink {
        let mut sink = Dict::new();
        for (key, value) in dict {
            if !used.contains(key.as_str()) {
                sink.insert(key, value);
            }
        }
        f(vm, expr, Value::Dict(sink))?;
    }

    Ok(())
}

/// The error message when the number of elements of the destructuring and the
/// array is mismatched.
#[cold]
fn wrong_number_of_elements(
    destruct: ast::Destructuring,
    len: usize,
) -> SourceDiagnostic {
    let mut count = 0;
    let mut spread = false;

    for p in destruct.items() {
        match p {
            ast::DestructuringItem::Pattern(_) => count += 1,
            ast::DestructuringItem::Spread(_) => spread = true,
            ast::DestructuringItem::Named(_) => {}
        }
    }

    let quantifier = if len > count { "too many" } else { "not enough" };
    let expected = match (spread, count) {
        (true, 1) => "at least 1 element".into(),
        (true, c) => eco_format!("at least {c} elements"),
        (false, 0) => "an empty array".into(),
        (false, 1) => "a single element".into(),
        (false, c) => eco_format!("{c} elements",),
    };

    error!(
        destruct.span(), "{quantifier} elements to destructure";
        hint: "the provided array has a length of {len}, \
               but the pattern expects {expected}";
    )
}
