#[path = "box.rs"]
mod box_;
mod collect;
mod deco;
mod finalize;
mod line;
mod linebreak;
mod prepare;
mod shaping;

pub use self::box_::layout_box;

use comemo::{Track, Tracked, TrackedMut};
use typst_library::diag::SourceResult;
use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{Packed, Resolve, Smart, StyleChain};
use typst_library::introspection::{Introspector, Locator, LocatorLink, SplitLocator};
use typst_library::layout::{Abs, AlignElem, Dir, FixedAlignment, Fragment, Size};
use typst_library::model::{
    EnumElem, FirstLineIndent, Linebreaks, ListElem, ParElem, ParLine, ParLineMarker,
    TermsElem,
};
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
use typst_library::text::{Costs, Lang, TextElem};
use typst_library::World;
use typst_utils::{Numeric, SliceExt};

use self::collect::{collect, Item, Segment, SpanMapper};
use self::deco::decorate;
use self::finalize::finalize;
use self::line::{apply_baseline_shift, commit, line, Line};
use self::linebreak::{linebreak, Breakpoint};
use self::prepare::{prepare, Preparation};
use self::shaping::{
    cjk_punct_style, is_of_cj_script, shape_range, ShapedGlyph, ShapedText,
    BEGIN_PUNCT_PAT, END_PUNCT_PAT,
};

/// Range of a substring of text.
type Range = std::ops::Range<usize>;

/// Layouts the paragraph.
pub fn layout_par(
    elem: &Packed<ParElem>,
    engine: &mut Engine,
    locator: Locator,
    styles: StyleChain,
    region: Size,
    expand: bool,
    situation: ParSituation,
) -> SourceResult<Fragment> {
    layout_par_impl(
        elem,
        engine.routines,
        engine.world,
        engine.introspector,
        engine.traced,
        TrackedMut::reborrow_mut(&mut engine.sink),
        engine.route.track(),
        locator.track(),
        styles,
        region,
        expand,
        situation,
    )
}

/// The internal, memoized implementation of `layout_par`.
#[comemo::memoize]
#[allow(clippy::too_many_arguments)]
fn layout_par_impl(
    elem: &Packed<ParElem>,
    routines: &Routines,
    world: Tracked<dyn World + '_>,
    introspector: Tracked<Introspector>,
    traced: Tracked<Traced>,
    sink: TrackedMut<Sink>,
    route: Tracked<Route>,
    locator: Tracked<Locator>,
    styles: StyleChain,
    region: Size,
    expand: bool,
    situation: ParSituation,
) -> SourceResult<Fragment> {
    let link = LocatorLink::new(locator);
    let mut locator = Locator::link(&link).split();
    let mut engine = Engine {
        routines,
        world,
        introspector,
        traced,
        sink,
        route: Route::extend(route),
    };

    let arenas = Arenas::default();
    let children = (engine.routines.realize)(
        RealizationKind::LayoutPar,
        &mut engine,
        &mut locator,
        &arenas,
        &elem.body,
        styles,
    )?;

    layout_inline_impl(
        &mut engine,
        &children,
        &mut locator,
        styles,
        region,
        expand,
        Some(situation),
        &ConfigBase {
            justify: elem.justify(styles),
            linebreaks: elem.linebreaks(styles),
            first_line_indent: elem.first_line_indent(styles),
            hanging_indent: elem.hanging_indent(styles),
        },
    )
}

/// Lays out realized content with inline layout.
pub fn layout_inline<'a>(
    engine: &mut Engine,
    children: &[Pair<'a>],
    locator: &mut SplitLocator<'a>,
    shared: StyleChain<'a>,
    region: Size,
    expand: bool,
) -> SourceResult<Fragment> {
    layout_inline_impl(
        engine,
        children,
        locator,
        shared,
        region,
        expand,
        None,
        &ConfigBase {
            justify: ParElem::justify_in(shared),
            linebreaks: ParElem::linebreaks_in(shared),
            first_line_indent: ParElem::first_line_indent_in(shared),
            hanging_indent: ParElem::hanging_indent_in(shared),
        },
    )
}

/// The internal implementation of [`layout_inline`].
#[allow(clippy::too_many_arguments)]
fn layout_inline_impl<'a>(
    engine: &mut Engine,
    children: &[Pair<'a>],
    locator: &mut SplitLocator<'a>,
    shared: StyleChain<'a>,
    region: Size,
    expand: bool,
    par: Option<ParSituation>,
    base: &ConfigBase,
) -> SourceResult<Fragment> {
    // Prepare configuration that is shared across the whole inline layout.
    let config = configuration(base, children, shared, par);

    // Collect all text into one string for BiDi analysis.
    let (text, segments, spans) = collect(children, engine, locator, &config, region)?;

    // Perform BiDi analysis and performs some preparation steps before we
    // proceed to line breaking.
    let p = prepare(engine, &config, &text, segments, spans)?;

    // Break the text into lines.
    let lines = linebreak(engine, &p, region.x - config.hanging_indent);

    // Turn the selected lines into frames.
    finalize(engine, &p, &lines, region, expand, locator)
}

/// Determine the inline layout's configuration.
fn configuration(
    base: &ConfigBase,
    children: &[Pair],
    shared: StyleChain,
    situation: Option<ParSituation>,
) -> Config {
    let justify = base.justify;
    let font_size = TextElem::size_in(shared);
    let dir = TextElem::dir_in(shared);

    Config {
        justify,
        linebreaks: base.linebreaks.unwrap_or_else(|| {
            if justify {
                Linebreaks::Optimized
            } else {
                Linebreaks::Simple
            }
        }),
        first_line_indent: {
            let FirstLineIndent { amount, all } = base.first_line_indent;
            if !amount.is_zero()
                && match situation {
                    // First-line indent for the first paragraph after a list
                    // bullet just looks bad.
                    Some(ParSituation::First) => all && !in_list(shared),
                    Some(ParSituation::Consecutive) => true,
                    Some(ParSituation::Other) => all,
                    None => false,
                }
                && AlignElem::alignment_in(shared).resolve(shared).x == dir.start().into()
            {
                amount.at(font_size)
            } else {
                Abs::zero()
            }
        },
        hanging_indent: if situation.is_some() {
            base.hanging_indent
        } else {
            Abs::zero()
        },
        numbering_marker: ParLine::numbering_in(shared).map(|numbering| {
            Packed::new(ParLineMarker::new(
                numbering,
                ParLine::number_align_in(shared),
                ParLine::number_margin_in(shared),
                // Delay resolving the number clearance until line numbers are
                // laid out to avoid inconsistent spacing depending on varying
                // font size.
                ParLine::number_clearance_in(shared),
            ))
        }),
        align: AlignElem::alignment_in(shared).fix(dir).x,
        font_size,
        dir,
        hyphenate: shared_get(children, shared, TextElem::hyphenate_in)
            .map(|uniform| uniform.unwrap_or(justify)),
        lang: shared_get(children, shared, TextElem::lang_in),
        fallback: TextElem::fallback_in(shared),
        cjk_latin_spacing: TextElem::cjk_latin_spacing_in(shared).is_auto(),
        costs: TextElem::costs_in(shared),
    }
}

/// Distinguishes between a few different kinds of paragraphs.
///
/// In the form `Option<ParSituation>`, `None` implies that we are creating an
/// inline layout that isn't a semantic paragraph.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum ParSituation {
    /// The paragraph is the first thing in the flow.
    First,
    /// The paragraph follows another paragraph.
    Consecutive,
    /// Any other kind of paragraph.
    Other,
}

/// Raw values from a `ParElem` or style chain. Used to initialize a [`Config`].
struct ConfigBase {
    justify: bool,
    linebreaks: Smart<Linebreaks>,
    first_line_indent: FirstLineIndent,
    hanging_indent: Abs,
}

/// Shared configuration for the whole inline layout.
struct Config {
    /// Whether to justify text.
    justify: bool,
    /// How to determine line breaks.
    linebreaks: Linebreaks,
    /// The indent the first line of a paragraph should have.
    first_line_indent: Abs,
    /// The indent that all but the first line of a paragraph should have.
    hanging_indent: Abs,
    /// Configuration for line numbering.
    numbering_marker: Option<Packed<ParLineMarker>>,
    /// The resolved horizontal alignment.
    align: FixedAlignment,
    /// The text size.
    font_size: Abs,
    /// The dominant direction.
    dir: Dir,
    /// A uniform hyphenation setting (only `Some(_)` if it's the same for all
    /// children, otherwise `None`).
    hyphenate: Option<bool>,
    /// The text language (only `Some(_)` if it's the same for all
    /// children, otherwise `None`).
    lang: Option<Lang>,
    /// Whether font fallback is enabled.
    fallback: bool,
    /// Whether to add spacing between CJK and Latin characters.
    cjk_latin_spacing: bool,
    /// Costs for various layout decisions.
    costs: Costs,
}

/// Get a style property, but only if it is the same for all of the children.
fn shared_get<T: PartialEq>(
    children: &[Pair],
    styles: StyleChain<'_>,
    getter: fn(StyleChain) -> T,
) -> Option<T> {
    let value = getter(styles);
    children
        .group_by_key(|&(_, s)| s)
        .all(|(s, _)| getter(s) == value)
        .then_some(value)
}

/// Whether we have a list ancestor.
///
/// When we support some kind of more general ancestry mechanism, this can
/// become more elegant.
fn in_list(styles: StyleChain) -> bool {
    ListElem::depth_in(styles).0 > 0
        || !EnumElem::parents_in(styles).is_empty()
        || TermsElem::within_in(styles)
}
