use std::collections::HashMap;
use std::sync::LazyLock;

use bumpalo::Bump;
use comemo::Tracked;

use crate::engine::Engine;
use crate::foundations::{
    Args, CastInfo, Content, Context, Func, IntoValue, NativeElement, NativeFunc,
    NativeFuncData, NativeFuncPtr, NativeParamInfo, Reflect, Scope, SymbolElem, Type,
    elem, func,
};
use crate::layout::{Em, Length, Rel};
use crate::math::Mathy;

/// How much less high scaled delimiters can be than what they wrap.
pub const DELIM_SHORT_FALL: Em = Em::new(0.1);

/// Scales delimiters.
///
/// While matched delimiters scale by default, this can be used to scale
/// unmatched delimiters and to control the delimiter scaling more precisely.
#[elem(title = "Left/Right", Mathy)]
pub struct LrElem {
    /// The size of the brackets, relative to the height of the wrapped content.
    #[default(Rel::one())]
    pub size: Rel<Length>,

    /// The delimited content, including the delimiters.
    #[required]
    #[parse(
        let mut arguments = args.all::<Content>()?.into_iter();
        let mut body = arguments.next().unwrap_or_default();
        arguments.for_each(|arg| body += SymbolElem::packed(',') + arg);
        body
    )]
    pub body: Content,
}

/// Scales delimiters vertically to the nearest surrounding `{lr()}` group.
///
/// ```example
/// $ { x mid(|) sum_(i=1)^n w_i|f_i (x)| < 1 } $
/// ```
#[elem(Mathy)]
pub struct MidElem {
    /// The content to be scaled.
    #[required]
    pub body: Content,
}

/// Floors an expression.
///
/// ```example
/// $ floor(x/2) $
/// ```
#[func]
pub fn floor(
    /// The size of the brackets, relative to the height of the wrapped content.
    ///
    /// Default: The current value of @math.lr.size[`lr.size`].
    #[named]
    size: Option<Rel<Length>>,
    /// The expression to floor.
    body: Content,
) -> Content {
    delimited(body, '⌊', '⌋', size)
}

/// Ceils an expression.
///
/// ```example
/// $ ceil(x/2) $
/// ```
#[func]
pub fn ceil(
    /// The size of the brackets, relative to the height of the wrapped content.
    ///
    /// Default: The current value of @math.lr.size[`lr.size`].
    #[named]
    size: Option<Rel<Length>>,
    /// The expression to ceil.
    body: Content,
) -> Content {
    delimited(body, '⌈', '⌉', size)
}

/// Rounds an expression.
///
/// ```example
/// $ round(x/2) $
/// ```
#[func]
pub fn round(
    /// The size of the brackets, relative to the height of the wrapped content.
    ///
    /// Default: The current value of @math.lr.size[`lr.size`].
    #[named]
    size: Option<Rel<Length>>,
    /// The expression to round.
    body: Content,
) -> Content {
    delimited(body, '⌊', '⌉', size)
}

/// Takes the absolute value of an expression.
///
/// ```example
/// $ abs(x/2) $
/// ```
#[func]
pub fn abs(
    /// The size of the brackets, relative to the height of the wrapped content.
    ///
    /// Default: The current value of @math.lr.size[`lr.size`].
    #[named]
    size: Option<Rel<Length>>,
    /// The expression to take the absolute value of.
    body: Content,
) -> Content {
    delimited(body, '|', '|', size)
}

/// Takes the norm of an expression.
///
/// ```example
/// $ norm(x/2) $
/// ```
#[func]
pub fn norm(
    /// The size of the brackets, relative to the height of the wrapped content.
    ///
    /// Default: The current value of @math.lr.size[`lr.size`].
    #[named]
    size: Option<Rel<Length>>,
    /// The expression to take the norm of.
    body: Content,
) -> Content {
    delimited(body, '‖', '‖', size)
}

/// Gets the Left/Right wrapper function corresponding to a symbol value, if
/// any.
pub fn get_lr_wrapper_func(value: &str) -> Option<Func> {
    let left = value.parse::<char>().ok()?;
    match left {
        // Unlike `round`, `abs`, and `norm`, `floor` and `ceil` are of type
        // `symbol` and cast to a function like other L/R symbols. We could thus
        // rely on autogeneration for these as well, but since they are
        // specifically called out in the documentation on the L/R page (via the
        // group mechanism), it's nice for them to have a bit of extra
        // documentation.
        '⌈' => Some(ceil::func()),
        '⌊' => Some(floor::func()),
        l => FUNCS.get(&l).map(Func::from),
    }
}

/// The delimiter pairings supported for use as callable symbols.
const DELIMS: &[(char, char)] = &[
    // The `ceil` and `floor` pairs are omitted here because they are handled
    // manually.
    ('(', ')'),
    ('⟮', '⟯'),
    ('⦇', '⦈'),
    ('⦅', '⦆'),
    ('⦓', '⦔'),
    ('⦕', '⦖'),
    ('{', '}'),
    ('⦃', '⦄'),
    ('[', ']'),
    ('⦍', '⦐'),
    ('⦏', '⦎'),
    ('⟦', '⟧'),
    ('⦋', '⦌'),
    ('❲', '❳'),
    ('⟬', '⟭'),
    ('⦗', '⦘'),
    ('⟅', '⟆'),
    ('⎰', '⎱'),
    ('⎱', '⎰'),
    ('⧘', '⧙'),
    ('⧚', '⧛'),
    ('⟨', '⟩'),
    ('⧼', '⧽'),
    ('⦑', '⦒'),
    ('⦉', '⦊'),
    ('⟪', '⟫'),
    ('⌜', '⌝'),
    ('⌞', '⌟'),
    // Fences.
    ('|', '|'),
    ('‖', '‖'),
    ('⦀', '⦀'),
    ('⦙', '⦙'),
    ('⦚', '⦚'),
];

/// Lazily created left/right wrapper functions.
static FUNCS: LazyLock<HashMap<char, NativeFuncData>> = LazyLock::new(|| {
    let bump = Box::leak(Box::new(Bump::new()));
    DELIMS
        .iter()
        .copied()
        .map(|(l, r)| (l, create_lr_func_data(l, r, bump)))
        .collect()
});

/// Creates metadata for an L/R wrapper function.
fn create_lr_func_data(left: char, right: char, bump: &'static Bump) -> NativeFuncData {
    let title = bumpalo::format!(in bump, "{}{} Left/Right", left, right).into_bump_str();
    let docs = bumpalo::format!(in bump, "Wraps an expression in {}{}.", left, right)
        .into_bump_str();
    NativeFuncData {
        function: NativeFuncPtr(bump.alloc(
            move |_: &mut Engine, _: Tracked<Context>, args: &mut Args| {
                let size = args.named("size")?;
                let body = args.expect("body")?;
                Ok(delimited(body, left, right, size).into_value())
            },
        )),
        name: "(..) => ..",
        title,
        docs,
        def_site: None,
        keywords: &[],
        contextual: false,
        scope: LazyLock::new(&|| Scope::new()),
        params: LazyLock::new(&|| create_lr_param_info()),
        returns: LazyLock::new(&|| CastInfo::Type(Type::of::<Content>())),
    }
}

/// Creates parameter signature metadata for an L/R function.
fn create_lr_param_info() -> Vec<NativeParamInfo> {
    vec![
        NativeParamInfo {
            name: "size",
            docs: "\
            The size of the brackets, relative to the height of the wrapped content.\n\
            \n\
            Default: The current value of [`lr.size`]($math.lr.size).",
            def_site: None,
            input: Rel::<Length>::input(),
            default: None,
            positional: false,
            named: true,
            variadic: false,
            required: false,
            settable: false,
        },
        NativeParamInfo {
            name: "body",
            docs: "The expression to wrap.",
            def_site: None,
            input: Content::input(),
            default: None,
            positional: true,
            named: false,
            variadic: false,
            required: true,
            settable: false,
        },
    ]
}

/// Creates an L/R element with the given delimiters.
fn delimited(
    body: Content,
    left: char,
    right: char,
    size: Option<Rel<Length>>,
) -> Content {
    let span = body.span();
    let mut elem = LrElem::new(Content::sequence([
        SymbolElem::packed(left),
        body,
        SymbolElem::packed(right),
    ]));
    // Push size only if size is provided
    if let Some(size) = size {
        elem.size.set(size);
    }
    elem.pack().spanned(span)
}
