use typst_library::diag::SourceResult;
use typst_library::foundations::{Content, Packed, Resolve, StyleChain};
use typst_library::layout::{Abs, Em, FixedAlignment, Frame, FrameItem, Point, Size};
use typst_library::math::{
    OverbraceElem, OverbracketElem, OverlineElem, OverparenElem, OvershellElem,
    UnderbraceElem, UnderbracketElem, UnderlineElem, UnderparenElem, UndershellElem,
};
use typst_library::text::TextElem;
use typst_library::visualize::{FixedStroke, Geometry};
use typst_syntax::Span;

use super::{
    stack, style_cramped, style_for_subscript, style_for_superscript, FrameFragment,
    GlyphFragment, LeftRightAlternator, MathContext, MathRun,
};

const BRACE_GAP: Em = Em::new(0.25);
const BRACKET_GAP: Em = Em::new(0.25);
const PAREN_GAP: Em = Em::new(0.25);
const SHELL_GAP: Em = Em::new(0.25);

/// A marker to distinguish under- and overlines.
enum Position {
    Under,
    Over,
}

/// Lays out an [`UnderlineElem`].
#[typst_macros::time(name = "math.underline", span = elem.span())]
pub fn layout_underline(
    elem: &Packed<UnderlineElem>,
    ctx: &mut MathContext,
    styles: StyleChain,
) -> SourceResult<()> {
    layout_underoverline(ctx, styles, &elem.body, elem.span(), Position::Under)
}

/// Lays out an [`OverlineElem`].
#[typst_macros::time(name = "math.overline", span = elem.span())]
pub fn layout_overline(
    elem: &Packed<OverlineElem>,
    ctx: &mut MathContext,
    styles: StyleChain,
) -> SourceResult<()> {
    layout_underoverline(ctx, styles, &elem.body, elem.span(), Position::Over)
}

/// Lays out an [`UnderbraceElem`].
#[typst_macros::time(name = "math.underbrace", span = elem.span())]
pub fn layout_underbrace(
    elem: &Packed<UnderbraceElem>,
    ctx: &mut MathContext,
    styles: StyleChain,
) -> SourceResult<()> {
    layout_underoverspreader(
        ctx,
        styles,
        &elem.body,
        &elem.annotation(styles),
        '⏟',
        BRACE_GAP,
        Position::Under,
        elem.span(),
    )
}

/// Lays out an [`OverbraceElem`].
#[typst_macros::time(name = "math.overbrace", span = elem.span())]
pub fn layout_overbrace(
    elem: &Packed<OverbraceElem>,
    ctx: &mut MathContext,
    styles: StyleChain,
) -> SourceResult<()> {
    layout_underoverspreader(
        ctx,
        styles,
        &elem.body,
        &elem.annotation(styles),
        '⏞',
        BRACE_GAP,
        Position::Over,
        elem.span(),
    )
}

/// Lays out an [`UnderbracketElem`].
#[typst_macros::time(name = "math.underbracket", span = elem.span())]
pub fn layout_underbracket(
    elem: &Packed<UnderbracketElem>,
    ctx: &mut MathContext,
    styles: StyleChain,
) -> SourceResult<()> {
    layout_underoverspreader(
        ctx,
        styles,
        &elem.body,
        &elem.annotation(styles),
        '⎵',
        BRACKET_GAP,
        Position::Under,
        elem.span(),
    )
}

/// Lays out an [`OverbracketElem`].
#[typst_macros::time(name = "math.overbracket", span = elem.span())]
pub fn layout_overbracket(
    elem: &Packed<OverbracketElem>,
    ctx: &mut MathContext,
    styles: StyleChain,
) -> SourceResult<()> {
    layout_underoverspreader(
        ctx,
        styles,
        &elem.body,
        &elem.annotation(styles),
        '⎴',
        BRACKET_GAP,
        Position::Over,
        elem.span(),
    )
}

/// Lays out an [`UnderparenElem`].
#[typst_macros::time(name = "math.underparen", span = elem.span())]
pub fn layout_underparen(
    elem: &Packed<UnderparenElem>,
    ctx: &mut MathContext,
    styles: StyleChain,
) -> SourceResult<()> {
    layout_underoverspreader(
        ctx,
        styles,
        &elem.body,
        &elem.annotation(styles),
        '⏝',
        PAREN_GAP,
        Position::Under,
        elem.span(),
    )
}

/// Lays out an [`OverparenElem`].
#[typst_macros::time(name = "math.overparen", span = elem.span())]
pub fn layout_overparen(
    elem: &Packed<OverparenElem>,
    ctx: &mut MathContext,
    styles: StyleChain,
) -> SourceResult<()> {
    layout_underoverspreader(
        ctx,
        styles,
        &elem.body,
        &elem.annotation(styles),
        '⏜',
        PAREN_GAP,
        Position::Over,
        elem.span(),
    )
}

/// Lays out an [`UndershellElem`].
#[typst_macros::time(name = "math.undershell", span = elem.span())]
pub fn layout_undershell(
    elem: &Packed<UndershellElem>,
    ctx: &mut MathContext,
    styles: StyleChain,
) -> SourceResult<()> {
    layout_underoverspreader(
        ctx,
        styles,
        &elem.body,
        &elem.annotation(styles),
        '⏡',
        SHELL_GAP,
        Position::Under,
        elem.span(),
    )
}

/// Lays out an [`OvershellElem`].
#[typst_macros::time(name = "math.overshell", span = elem.span())]
pub fn layout_overshell(
    elem: &Packed<OvershellElem>,
    ctx: &mut MathContext,
    styles: StyleChain,
) -> SourceResult<()> {
    layout_underoverspreader(
        ctx,
        styles,
        &elem.body,
        &elem.annotation(styles),
        '⏠',
        SHELL_GAP,
        Position::Over,
        elem.span(),
    )
}

/// layout under- or overlined content.
fn layout_underoverline(
    ctx: &mut MathContext,
    styles: StyleChain,
    body: &Content,
    span: Span,
    position: Position,
) -> SourceResult<()> {
    let (extra_height, content, line_pos, content_pos, baseline, bar_height, line_adjust);
    match position {
        Position::Under => {
            let sep = scaled!(ctx, styles, underbar_extra_descender);
            bar_height = scaled!(ctx, styles, underbar_rule_thickness);
            let gap = scaled!(ctx, styles, underbar_vertical_gap);
            extra_height = sep + bar_height + gap;

            content = ctx.layout_into_fragment(body, styles)?;

            line_pos = Point::with_y(content.height() + gap + bar_height / 2.0);
            content_pos = Point::zero();
            baseline = content.ascent();
            line_adjust = -content.italics_correction();
        }
        Position::Over => {
            let sep = scaled!(ctx, styles, overbar_extra_ascender);
            bar_height = scaled!(ctx, styles, overbar_rule_thickness);
            let gap = scaled!(ctx, styles, overbar_vertical_gap);
            extra_height = sep + bar_height + gap;

            let cramped = style_cramped();
            content = ctx.layout_into_fragment(body, styles.chain(&cramped))?;

            line_pos = Point::with_y(sep + bar_height / 2.0);
            content_pos = Point::with_y(extra_height);
            baseline = content.ascent() + extra_height;
            line_adjust = Abs::zero();
        }
    }

    let width = content.width();
    let height = content.height() + extra_height;
    let size = Size::new(width, height);
    let line_width = width + line_adjust;

    let content_class = content.class();
    let content_is_text_like = content.is_text_like();
    let content_italics_correction = content.italics_correction();
    let mut frame = Frame::soft(size);
    frame.set_baseline(baseline);
    frame.push_frame(content_pos, content.into_frame());
    frame.push(
        line_pos,
        FrameItem::Shape(
            Geometry::Line(Point::with_x(line_width)).stroked(FixedStroke {
                paint: TextElem::fill_in(styles).as_decoration(),
                thickness: bar_height,
                ..FixedStroke::default()
            }),
            span,
        ),
    );

    ctx.push(
        FrameFragment::new(styles, frame)
            .with_class(content_class)
            .with_text_like(content_is_text_like)
            .with_italics_correction(content_italics_correction),
    );

    Ok(())
}

/// Layout an over- or underbrace-like object.
#[allow(clippy::too_many_arguments)]
fn layout_underoverspreader(
    ctx: &mut MathContext,
    styles: StyleChain,
    body: &Content,
    annotation: &Option<Content>,
    c: char,
    gap: Em,
    position: Position,
    span: Span,
) -> SourceResult<()> {
    let gap = gap.resolve(styles);
    let body = ctx.layout_into_run(body, styles)?;
    let body_class = body.class();
    let body = body.into_fragment(styles);
    let glyph = GlyphFragment::new(ctx, styles, c, span);
    let stretched = glyph.stretch_horizontal(ctx, body.width(), Abs::zero());

    let mut rows = vec![];
    let baseline = match position {
        Position::Under => {
            rows.push(MathRun::new(vec![body]));
            rows.push(stretched.into());
            if let Some(annotation) = annotation {
                let under_style = style_for_subscript(styles);
                let annotation_styles = styles.chain(&under_style);
                rows.extend(ctx.layout_into_run(annotation, annotation_styles)?.rows());
            }
            0
        }
        Position::Over => {
            if let Some(annotation) = annotation {
                let over_style = style_for_superscript(styles);
                let annotation_styles = styles.chain(&over_style);
                rows.extend(ctx.layout_into_run(annotation, annotation_styles)?.rows());
            }
            rows.push(stretched.into());
            rows.push(MathRun::new(vec![body]));
            rows.len() - 1
        }
    };

    let frame = stack(
        rows,
        FixedAlignment::Center,
        gap,
        baseline,
        LeftRightAlternator::Right,
        None,
    );
    ctx.push(FrameFragment::new(styles, frame).with_class(body_class));

    Ok(())
}
