//! Text handling.

mod deco;
mod misc;
mod quotes;
mod raw;
mod shaping;
mod shift;

pub use self::deco::*;
pub use self::misc::*;
pub use self::quotes::*;
pub use self::raw::*;
pub use self::shaping::*;
pub use self::shift::*;

use std::borrow::Cow;

use rustybuzz::Tag;
use typst::font::{FontMetrics, FontStretch, FontStyle, FontWeight, VerticalFontMetric};

use crate::layout::ParElem;
use crate::prelude::*;

/// Customize the look and layout of text in a variety of ways.
///
/// This function is used often, both with set rules and directly. While the set
/// rule is often the simpler choice, calling the text function directly can be
/// useful when passing text as an argument to another function.
///
/// ## Example
/// ```example
/// #set text(18pt)
/// With a set rule.
///
/// #emph(text(blue)[
///   With a function call.
/// ])
/// ```
///
/// Display: Text
/// Category: text
#[element(Construct, PlainText)]
pub struct TextElem {
    /// A prioritized sequence of font families.
    ///
    /// When processing text, Typst tries all specified font families in order
    /// until it finds a font that has the necessary glyphs. In the example
    /// below, the font `Inria Serif` is preferred, but since it does not
    /// contain Arabic glyphs, the arabic text uses `Noto Sans Arabic` instead.
    ///
    /// ```example
    /// #set text(font: (
    ///   "Inria Serif",
    ///   "Noto Sans Arabic",
    /// ))
    ///
    /// This is Latin. \
    /// هذا عربي.
    ///
    /// ```
    #[default(FontList(vec![FontFamily::new("Linux Libertine")]))]
    pub font: FontList,

    /// Whether to allow last resort font fallback when the primary font list
    /// contains no match. This lets Typst search through all available fonts
    /// for the most similar one that has the necessary glyphs.
    ///
    /// _Note:_ Currently, there are no warnings when fallback is disabled and
    /// no glyphs are found. Instead, your text shows up in the form of "tofus":
    /// Small boxes that indicate the lack of an appropriate glyph. In the
    /// future, you will be able to instruct Typst to issue warnings so you know
    /// something is up.
    ///
    /// ```example
    /// #set text(font: "Inria Serif")
    /// هذا عربي
    ///
    /// #set text(fallback: false)
    /// هذا عربي
    /// ```
    #[default(true)]
    pub fallback: bool,

    /// The desired font style.
    ///
    /// When an italic style is requested and only an oblique one is available,
    /// it is used. Similarly, the other way around, an italic style can stand
    /// in for an oblique one.  When neither an italic nor an oblique style is
    /// available, Typst selects the normal style. Since most fonts are only
    /// available either in an italic or oblique style, the difference between
    /// italic and oblique style is rarely observable.
    ///
    /// If you want to emphasize your text, you should do so using the
    /// [emph]($func/emph) function instead. This makes it easy to adapt the
    /// style later if you change your mind about how to signify the emphasis.
    ///
    /// ```example
    /// #text(font: "Linux Libertine", style: "italic")[Italic]
    /// #text(font: "DejaVu Sans", style: "oblique")[Oblique]
    /// ```
    pub style: FontStyle,

    /// The desired thickness of the font's glyphs. Accepts an integer between
    /// `{100}` and `{900}` or one of the predefined weight names. When the
    /// desired weight is not available, Typst selects the font from the family
    /// that is closest in weight.
    ///
    /// If you want to strongly emphasize your text, you should do so using the
    /// [strong]($func/strong) function instead. This makes it easy to adapt the
    /// style later if you change your mind about how to signify the strong
    /// emphasis.
    ///
    /// ```example
    /// #text(weight: "light")[Light] \
    /// #text(weight: "regular")[Regular] \
    /// #text(weight: "medium")[Medium] \
    /// #text(weight: 500)[Medium] \
    /// #text(weight: "bold")[Bold]
    /// ```
    pub weight: FontWeight,

    /// The desired width of the glyphs. Accepts a ratio between `{50%}` and
    /// `{200%}`. When the desired weight is not available, Typst selects the
    /// font from the family that is closest in stretch.
    ///
    /// ```example
    /// #text(stretch: 75%)[Condensed] \
    /// #text(stretch: 100%)[Normal]
    /// ```
    pub stretch: FontStretch,

    /// The size of the glyphs. This value forms the basis of the `em` unit:
    /// `{1em}` is equivalent to the font size.
    ///
    /// You can also give the font size itself in `em` units. Then, it is
    /// relative to the previous font size.
    ///
    /// ```example
    /// #set text(size: 20pt)
    /// very #text(1.5em)[big] text
    /// ```
    #[parse(args.named_or_find("size")?)]
    #[fold]
    #[default(Abs::pt(11.0))]
    pub size: TextSize,

    /// The glyph fill color.
    ///
    /// ```example
    /// #set text(fill: red)
    /// This text is red.
    /// ```
    #[parse(args.named_or_find("fill")?)]
    #[default(Color::BLACK.into())]
    pub fill: Paint,

    /// The amount of space that should be added between characters.
    ///
    /// ```example
    /// #set text(tracking: 1.5pt)
    /// Distant text.
    /// ```
    #[resolve]
    pub tracking: Length,

    /// The amount of space between words.
    ///
    /// Can be given as an absolute length, but also relative to the width of
    /// the space character in the font.
    ///
    /// ```example
    /// #set text(spacing: 200%)
    /// Text with distant words.
    /// ```
    #[resolve]
    #[default(Rel::one())]
    pub spacing: Rel<Length>,

    /// An amount to shift the text baseline by.
    ///
    /// ```example
    /// A #text(baseline: 3pt)[lowered]
    /// word.
    /// ```
    #[resolve]
    pub baseline: Length,

    /// Whether certain glyphs can hang over into the margin in justified text.
    /// This can make justification visually more pleasing.
    ///
    /// ```example
    /// #set par(justify: true)
    /// In this particular text, the
    /// justification produces a hyphen
    /// in the first line. Letting this
    /// hyphen hang slightly into the
    /// margin makes for a clear
    /// paragraph edge.
    ///
    /// #set text(overhang: false)
    /// In this particular text, the
    /// justification produces a hyphen
    /// in the first line. This time the
    /// hyphen does not hang into the
    /// margin, making the paragraph's
    /// edge less clear.
    /// ```
    #[default(true)]
    pub overhang: bool,

    /// The top end of the conceptual frame around the text used for layout and
    /// positioning. This affects the size of containers that hold text.
    ///
    /// ```example
    /// #set rect(inset: 0pt)
    /// #set text(size: 20pt)
    ///
    /// #set text(top-edge: "ascender")
    /// #rect(fill: aqua)[Typst]
    ///
    /// #set text(top-edge: "cap-height")
    /// #rect(fill: aqua)[Typst]
    /// ```
    #[default(TextEdge::Metric(VerticalFontMetric::CapHeight))]
    pub top_edge: TextEdge,

    /// The bottom end of the conceptual frame around the text used for layout
    /// and positioning. This affects the size of containers that hold text.
    ///
    /// ```example
    /// #set rect(inset: 0pt)
    /// #set text(size: 20pt)
    ///
    /// #set text(bottom-edge: "baseline")
    /// #rect(fill: aqua)[Typst]
    ///
    /// #set text(bottom-edge: "descender")
    /// #rect(fill: aqua)[Typst]
    /// ```
    #[default(TextEdge::Metric(VerticalFontMetric::Baseline))]
    pub bottom_edge: TextEdge,

    /// An [ISO 639-1/2/3 language code.](https://en.wikipedia.org/wiki/ISO_639)
    ///
    /// Setting the correct language affects various parts of Typst:
    ///
    /// - The text processing pipeline can make more informed choices.
    /// - Hyphenation will use the correct patterns for the language.
    /// - [Smart quotes]($func/smartquote) turns into the correct quotes for the
    ///   language.
    /// - And all other things which are language-aware.
    ///
    /// ```example
    /// #set text(lang: "de")
    /// #outline()
    ///
    /// = Einleitung
    /// In diesem Dokument, ...
    /// ```
    #[default(Lang::ENGLISH)]
    pub lang: Lang,

    /// An [ISO 3166-1 alpha-2 region code.](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)
    ///
    /// This lets the text processing pipeline make more informed choices.
    pub region: Option<Region>,

    /// The dominant direction for text and inline objects. Possible values are:
    ///
    /// - `{auto}`: Automatically infer the direction from the `lang` property.
    /// - `{ltr}`: Layout text from left to right.
    /// - `{rtl}`: Layout text from right to left.
    ///
    /// When writing in right-to-left scripts like Arabic or Hebrew, you should
    /// set the [text language]($func/text.lang) or direction. While individual
    /// runs of text are automatically layouted in the correct direction,
    /// setting the dominant direction gives the bidirectional reordering
    /// algorithm the necessary information to correctly place punctuation and
    /// inline objects. Furthermore, setting the direction affects the alignment
    /// values `start` and `end`, which are equivalent to `left` and `right` in
    /// `ltr` text and the other way around in `rtl` text.
    ///
    /// If you set this to `rtl` and experience bugs or in some way bad looking
    /// output, please do get in touch with us through the
    /// [contact form](https://typst.app/contact) or our
    /// [Discord server]($community/#discord)!
    ///
    /// ```example
    /// #set text(dir: rtl)
    /// هذا عربي.
    /// ```
    #[resolve]
    pub dir: TextDir,

    /// Whether to hyphenate text to improve line breaking. When `{auto}`, text
    /// will be hyphenated if and only if justification is enabled.
    ///
    /// Setting the [text language]($func/text.lang) ensures that the correct
    /// hyphenation patterns are used.
    ///
    /// ```example
    /// #set par(justify: true)
    /// This text illustrates how
    /// enabling hyphenation can
    /// improve justification.
    ///
    /// #set text(hyphenate: false)
    /// This text illustrates how
    /// enabling hyphenation can
    /// improve justification.
    /// ```
    #[resolve]
    pub hyphenate: Hyphenate,

    /// Whether to apply kerning.
    ///
    /// When enabled, specific letter pairings move closer together or further
    /// apart for a more visually pleasing result. The example below
    /// demonstrates how decreasing the gap between the "T" and "o" results in a
    /// more natural look. Setting this to `{false}` disables kerning by turning
    /// off the OpenType `kern` font feature.
    ///
    /// ```example
    /// #set text(size: 25pt)
    /// Totally
    ///
    /// #set text(kerning: false)
    /// Totally
    /// ```
    #[default(true)]
    pub kerning: bool,

    /// Whether to apply stylistic alternates.
    ///
    /// Sometimes fonts contain alternative glyphs for the same codepoint.
    /// Setting this to `{true}` switches to these by enabling the OpenType
    /// `salt` font feature.
    ///
    /// ```example
    /// #set text(size: 20pt)
    /// 0, a, g, ß
    ///
    /// #set text(alternates: true)
    /// 0, a, g, ß
    /// ```
    #[default(false)]
    pub alternates: bool,

    /// Which stylistic set to apply. Font designers can categorize alternative
    /// glyphs forms into stylistic sets. As this value is highly font-specific,
    /// you need to consult your font to know which sets are available. When set
    /// to an integer between `{1}` and `{20}`, enables the corresponding
    /// OpenType font feature from `ss01`, ..., `ss20`.
    pub stylistic_set: Option<StylisticSet>,

    /// Whether standard ligatures are active.
    ///
    /// Certain letter combinations like "fi" are often displayed as a single
    /// merged glyph called a _ligature._ Setting this to `{false}` disables
    /// these ligatures by turning off the OpenType `liga` and `clig` font
    /// features.
    ///
    /// ```example
    /// #set text(size: 20pt)
    /// A fine ligature.
    ///
    /// #set text(ligatures: false)
    /// A fine ligature.
    /// ```
    #[default(true)]
    pub ligatures: bool,

    /// Whether ligatures that should be used sparingly are active. Setting this
    /// to `{true}` enables the OpenType `dlig` font feature.
    #[default(false)]
    pub discretionary_ligatures: bool,

    /// Whether historical ligatures are active. Setting this to `{true}`
    /// enables the OpenType `hlig` font feature.
    #[default(false)]
    pub historical_ligatures: bool,

    /// Which kind of numbers / figures to select. When set to `{auto}`, the
    /// default numbers for the font are used.
    ///
    /// ```example
    /// #set text(font: "Noto Sans", 20pt)
    /// #set text(number-type: "lining")
    /// Number 9.
    ///
    /// #set text(number-type: "old-style")
    /// Number 9.
    /// ```
    pub number_type: Smart<NumberType>,

    /// The width of numbers / figures. When set to `{auto}`, the default
    /// numbers for the font are used.
    ///
    /// ```example
    /// #set text(font: "Noto Sans", 20pt)
    /// #set text(number-width: "proportional")
    /// A 12 B 34. \
    /// A 56 B 78.
    ///
    /// #set text(number-width: "tabular")
    /// A 12 B 34. \
    /// A 56 B 78.
    /// ```
    pub number_width: Smart<NumberWidth>,

    /// Whether to have a slash through the zero glyph. Setting this to `{true}`
    /// enables the OpenType `zero` font feature.
    ///
    /// ```example
    /// 0, #text(slashed-zero: true)[0]
    /// ```
    #[default(false)]
    pub slashed_zero: bool,

    /// Whether to turns numbers into fractions. Setting this to `{true}`
    /// enables the OpenType `frac` font feature.
    ///
    /// ```example
    /// 1/2 \
    /// #text(fractions: true)[1/2]
    /// ```
    #[default(false)]
    pub fractions: bool,

    /// Raw OpenType features to apply.
    ///
    /// - If given an array of strings, sets the features identified by the
    ///   strings to `{1}`.
    /// - If given a dictionary mapping to numbers, sets the features
    ///   identified by the keys to the values.
    ///
    /// ```example
    /// // Enable the `frac` feature manually.
    /// #set text(features: ("frac",))
    /// 1/2
    /// ```
    #[fold]
    pub features: FontFeatures,

    /// Content in which all text is styled according to the other arguments.
    #[external]
    #[required]
    pub body: Content,

    /// The text.
    #[internal]
    #[required]
    pub text: EcoString,

    /// A delta to apply on the font weight.
    #[internal]
    #[fold]
    pub delta: Delta,

    /// Whether the font style should be inverted.
    #[internal]
    #[fold]
    #[default(false)]
    pub emph: Toggle,

    /// Decorative lines.
    #[internal]
    #[fold]
    pub deco: Decoration,

    /// A case transformation that should be applied to the text.
    #[internal]
    pub case: Option<Case>,

    /// Whether small capital glyphs should be used. ("smcp")
    #[internal]
    #[default(false)]
    pub smallcaps: bool,
}

impl TextElem {
    /// Create a new packed text element.
    pub fn packed(text: impl Into<EcoString>) -> Content {
        Self::new(text.into()).pack()
    }
}

impl Construct for TextElem {
    fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
        // The text constructor is special: It doesn't create a text element.
        // Instead, it leaves the passed argument structurally unchanged, but
        // styles all text in it.
        let styles = Self::set(args)?;
        let body = args.expect::<Content>("body")?;
        Ok(body.styled_with_map(styles))
    }
}

impl PlainText for TextElem {
    fn plain_text(&self, text: &mut EcoString) {
        text.push_str(&self.text());
    }
}

/// A lowercased font family like "arial".
#[derive(Clone, Eq, PartialEq, Hash)]
pub struct FontFamily(EcoString);

impl FontFamily {
    /// Create a named font family variant.
    pub fn new(string: &str) -> Self {
        Self(string.to_lowercase().into())
    }

    /// The lowercased family name.
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl Debug for FontFamily {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        self.0.fmt(f)
    }
}

cast_from_value! {
    FontFamily,
    string: EcoString => Self::new(&string),
}

cast_to_value! {
    v: FontFamily => v.0.into()
}

/// Font family fallback list.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct FontList(pub Vec<FontFamily>);

impl IntoIterator for FontList {
    type IntoIter = std::vec::IntoIter<FontFamily>;
    type Item = FontFamily;

    fn into_iter(self) -> Self::IntoIter {
        self.0.into_iter()
    }
}

cast_from_value! {
    FontList,
    family: FontFamily => Self(vec![family]),
    values: Array => Self(values.into_iter().map(|v| v.cast()).collect::<StrResult<_>>()?),
}

cast_to_value! {
    v: FontList => v.0.into()
}

/// The size of text.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct TextSize(pub Length);

impl Fold for TextSize {
    type Output = Abs;

    fn fold(self, outer: Self::Output) -> Self::Output {
        self.0.em.at(outer) + self.0.abs
    }
}

cast_from_value! {
    TextSize,
    v: Length => Self(v),
}

cast_to_value! {
    v: TextSize => v.0.into()
}

/// Specifies the bottom or top edge of text.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum TextEdge {
    /// An edge specified using one of the well-known font metrics.
    Metric(VerticalFontMetric),
    /// An edge specified as a length.
    Length(Length),
}

impl TextEdge {
    /// Resolve the value of the text edge given a font's metrics.
    pub fn resolve(self, styles: StyleChain, metrics: &FontMetrics) -> Abs {
        match self {
            Self::Metric(metric) => metrics.vertical(metric).resolve(styles),
            Self::Length(length) => length.resolve(styles),
        }
    }
}

cast_from_value! {
    TextEdge,
    v: VerticalFontMetric => Self::Metric(v),
    v: Length => Self::Length(v),
}

cast_to_value! {
    v: TextEdge => match v {
        TextEdge::Metric(metric) => metric.into(),
        TextEdge::Length(length) => length.into(),
    }
}

/// The direction of text and inline objects in their line.
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
pub struct TextDir(pub Smart<Dir>);

cast_from_value! {
    TextDir,
    v: Smart<Dir> => {
        if v.map_or(false, |dir| dir.axis() == Axis::Y) {
            Err("text direction must be horizontal")?;
        }
        Self(v)
    },
}

cast_to_value! {
    v: TextDir => v.0.into()
}

impl Resolve for TextDir {
    type Output = Dir;

    fn resolve(self, styles: StyleChain) -> Self::Output {
        match self.0 {
            Smart::Auto => TextElem::lang_in(styles).dir(),
            Smart::Custom(dir) => dir,
        }
    }
}

/// Whether to hyphenate text.
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Hyphenate(pub Smart<bool>);

cast_from_value! {
    Hyphenate,
    v: Smart<bool> => Self(v),
}

cast_to_value! {
    v: Hyphenate => v.0.into()
}

impl Resolve for Hyphenate {
    type Output = bool;

    fn resolve(self, styles: StyleChain) -> Self::Output {
        match self.0 {
            Smart::Auto => ParElem::justify_in(styles),
            Smart::Custom(v) => v,
        }
    }
}

/// A stylistic set in a font.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct StylisticSet(u8);

impl StylisticSet {
    /// Create a new set, clamping to 1-20.
    pub fn new(index: u8) -> Self {
        Self(index.clamp(1, 20))
    }

    /// Get the value, guaranteed to be 1-20.
    pub fn get(self) -> u8 {
        self.0
    }
}

cast_from_value! {
    StylisticSet,
    v: i64 => match v {
        1 ..= 20 => Self::new(v as u8),
        _ => Err("stylistic set must be between 1 and 20")?,
    },
}

cast_to_value! {
    v: StylisticSet => v.0.into()
}

/// Which kind of numbers / figures to select.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum NumberType {
    /// Numbers that fit well with capital text (the OpenType `lnum`
    /// font feature).
    Lining,
    /// Numbers that fit well into a flow of upper- and lowercase text (the
    /// OpenType `onum` font feature).
    OldStyle,
}

/// The width of numbers / figures.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum NumberWidth {
    /// Numbers with glyph-specific widths (the OpenType `pnum` font feature).
    Proportional,
    /// Numbers of equal width (the OpenType `tnum` font feature).
    Tabular,
}

/// OpenType font features settings.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct FontFeatures(pub Vec<(Tag, u32)>);

cast_from_value! {
    FontFeatures,
    values: Array => Self(values
        .into_iter()
        .map(|v| {
            let tag = v.cast::<EcoString>()?;
            Ok((Tag::from_bytes_lossy(tag.as_bytes()), 1))
        })
        .collect::<StrResult<_>>()?),
    values: Dict => Self(values
        .into_iter()
        .map(|(k, v)| {
            let num = v.cast::<u32>()?;
            let tag = Tag::from_bytes_lossy(k.as_bytes());
            Ok((tag, num))
        })
        .collect::<StrResult<_>>()?),
}

cast_to_value! {
    v: FontFeatures => Value::Dict(
        v.0.into_iter()
            .map(|(tag, num)| {
                let bytes = tag.to_bytes();
                let key = std::str::from_utf8(&bytes).unwrap_or_default();
                (key.into(), num.into())
            })
            .collect(),
    )
}

impl Fold for FontFeatures {
    type Output = Self;

    fn fold(mut self, outer: Self::Output) -> Self::Output {
        self.0.extend(outer.0);
        self
    }
}
