use crate::FontResolverFn;
use crate::font::blob::{CffFontBlob, OpenTypeFontBlob};
use crate::font::generated::{glyph_names, metrics, standard, symbol, zapf_dings};
use crate::font::true_type::{Width, read_encoding, read_widths};
use crate::font::{
    Encoding, FontData, FontQuery, glyph_name_to_unicode, normalized_glyph_name, stretch_glyph,
    strip_subset_prefix,
};
use kurbo::BezPath;
use pdf_syntax::object::Dict;
use pdf_syntax::object::Name;
use pdf_syntax::object::dict::keys::{
    BASE_FONT, FONT_DESC, FONT_FAMILY, FONT_WEIGHT, ITALIC_ANGLE, MISSING_WIDTH,
};
use skrifa::raw::TableProvider;
use skrifa::{GlyphId, GlyphId16};
use std::cell::RefCell;
use std::collections::HashMap;

/// The 14 standard fonts of PDF.
#[derive(Copy, Clone, Debug)]
pub enum StandardFont {
    /// Helvetica.
    Helvetica,
    /// Helvetica Bold.
    HelveticaBold,
    /// Helvetica Oblique.
    HelveticaOblique,
    /// Helvetica Bold Oblique.
    HelveticaBoldOblique,
    /// Courier.
    Courier,
    /// Courier Bold.
    CourierBold,
    /// Courier Oblique.
    CourierOblique,
    /// Courier Bold Oblique.
    CourierBoldOblique,
    /// Times Roman.
    TimesRoman,
    /// Times Bold.
    TimesBold,
    /// Times Italic.
    TimesItalic,
    /// Times Bold Italic.
    TimesBoldItalic,
    /// Zapf Dingbats - a decorative symbol font.
    ZapfDingBats,
    /// Symbol - a mathematical symbol font.
    Symbol,
}

impl StandardFont {
    pub(crate) fn code_to_name(&self, code: u8) -> Option<&'static str> {
        match self {
            Self::Symbol => symbol::get(code),
            // Note that this font does not return postscript character names,
            // but instead has a custom encoding.
            Self::ZapfDingBats => zapf_dings::get(code),
            _ => standard::get(code),
        }
    }

    pub(crate) fn get_width(&self, mut name: &str) -> Option<f32> {
        // <https://github.com/apache/pdfbox/blob/129aafe26548c1ff935af9c55cb40a996186c35f/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDSimpleFont.java#L340>
        if name == ".notdef" {
            return Some(250.0);
        }

        name = normalized_glyph_name(name);

        match self {
            Self::Helvetica => metrics::HELVETICA.get(name).copied(),
            Self::HelveticaBold => metrics::HELVETICA_BOLD.get(name).copied(),
            Self::HelveticaOblique => metrics::HELVETICA_OBLIQUE.get(name).copied(),
            Self::HelveticaBoldOblique => metrics::HELVETICA_BOLD_OBLIQUE.get(name).copied(),
            Self::Courier => metrics::COURIER.get(name).copied(),
            Self::CourierBold => metrics::COURIER_BOLD.get(name).copied(),
            Self::CourierOblique => metrics::COURIER_OBLIQUE.get(name).copied(),
            Self::CourierBoldOblique => metrics::COURIER_BOLD_OBLIQUE.get(name).copied(),
            Self::TimesRoman => metrics::TIMES_ROMAN.get(name).copied(),
            Self::TimesBold => metrics::TIMES_BOLD.get(name).copied(),
            Self::TimesItalic => metrics::TIMES_ITALIC.get(name).copied(),
            Self::TimesBoldItalic => metrics::TIMES_BOLD_ITALIC.get(name).copied(),
            Self::ZapfDingBats => metrics::ZAPF_DING_BATS.get(name).copied(),
            Self::Symbol => metrics::SYMBOL.get(name).copied(),
        }
    }

    pub(crate) fn as_str(&self) -> &'static str {
        match self {
            Self::Helvetica => "Helvetica",
            Self::HelveticaBold => "Helvetica Bold",
            Self::HelveticaOblique => "Helvetica Oblique",
            Self::HelveticaBoldOblique => "Helvetica Bold Oblique",
            Self::Courier => "Courier",
            Self::CourierBold => "Courier Bold",
            Self::CourierOblique => "Courier Oblique",
            Self::CourierBoldOblique => "Courier Bold Oblique",
            Self::TimesRoman => "Times Roman",
            Self::TimesBold => "Times Bold",
            Self::TimesItalic => "Times Italic",
            Self::TimesBoldItalic => "Times Bold Italic",
            Self::ZapfDingBats => "Zapf Dingbats",
            Self::Symbol => "Symbol",
        }
    }

    /// Return the postscrit name of the font.
    pub fn postscript_name(&self) -> &'static str {
        match self {
            Self::Helvetica => "Helvetica",
            Self::HelveticaBold => "Helvetica-Bold",
            Self::HelveticaOblique => "Helvetica-Oblique",
            Self::HelveticaBoldOblique => "Helvetica-BoldOblique",
            Self::Courier => "Courier",
            Self::CourierBold => "Courier-Bold",
            Self::CourierOblique => "Courier-Oblique",
            Self::CourierBoldOblique => "Courier-BoldOblique",
            Self::TimesRoman => "Times-Roman",
            Self::TimesBold => "Times-Bold",
            Self::TimesItalic => "Times-Italic",
            Self::TimesBoldItalic => "Times-BoldItalic",
            Self::ZapfDingBats => "ZapfDingbats",
            Self::Symbol => "Symbol",
        }
    }

    pub(crate) fn is_bold(&self) -> bool {
        matches!(
            self,
            Self::HelveticaBold
                | Self::HelveticaBoldOblique
                | Self::CourierBold
                | Self::CourierBoldOblique
                | Self::TimesBold
                | Self::TimesBoldItalic
        )
    }

    pub(crate) fn is_italic(&self) -> bool {
        matches!(
            self,
            Self::HelveticaOblique
                | Self::HelveticaBoldOblique
                | Self::CourierOblique
                | Self::CourierBoldOblique
                | Self::TimesItalic
                | Self::TimesBoldItalic
        )
    }

    pub(crate) fn is_serif(&self) -> bool {
        matches!(
            self,
            Self::TimesRoman | Self::TimesBold | Self::TimesItalic | Self::TimesBoldItalic
        )
    }

    pub(crate) fn is_monospace(&self) -> bool {
        matches!(
            self,
            Self::Courier | Self::CourierBold | Self::CourierOblique | Self::CourierBoldOblique
        )
    }

    /// Return suitable font data for the given standard font.
    ///
    /// Currently, this will return the corresponding Foxit font, which is a set of permissibly
    /// licensed fonts that is also very light-weight.
    ///
    /// You can use the result of this method in your implementation of [`FontResolverFn`].
    ///
    /// [`FontResolverFn`]: crate::FontResolverFn
    #[cfg(feature = "embed-fonts")]
    pub fn get_font_data(&self) -> (FontData, u32) {
        use std::sync::Arc;

        let data = match self {
            Self::Helvetica => &include_bytes!("../../assets/FoxitSans.pfb")[..],
            Self::HelveticaBold => &include_bytes!("../../assets/FoxitSansBold.pfb")[..],
            Self::HelveticaOblique => &include_bytes!("../../assets/FoxitSansItalic.pfb")[..],
            Self::HelveticaBoldOblique => {
                &include_bytes!("../../assets/FoxitSansBoldItalic.pfb")[..]
            }
            Self::Courier => &include_bytes!("../../assets/FoxitFixed.pfb")[..],
            Self::CourierBold => &include_bytes!("../../assets/FoxitFixedBold.pfb")[..],
            Self::CourierOblique => &include_bytes!("../../assets/FoxitFixedItalic.pfb")[..],
            Self::CourierBoldOblique => {
                &include_bytes!("../../assets/FoxitFixedBoldItalic.pfb")[..]
            }
            Self::TimesRoman => &include_bytes!("../../assets/FoxitSerif.pfb")[..],
            Self::TimesBold => &include_bytes!("../../assets/FoxitSerifBold.pfb")[..],
            Self::TimesItalic => &include_bytes!("../../assets/FoxitSerifItalic.pfb")[..],
            Self::TimesBoldItalic => &include_bytes!("../../assets/FoxitSerifBoldItalic.pfb")[..],
            Self::ZapfDingBats => &include_bytes!("../../assets/FoxitDingbats.pfb")[..],
            Self::Symbol => {
                include_bytes!("../../assets/FoxitSymbol.pfb")
            }
        };

        (Arc::new(data), 0)
    }
}

enum StandardFontFamily {
    Helvetica,
    Courier,
    Times,
}

/// PostScript-name aliases commonly produced by Office/iText/etc. that refer
/// to fonts the reader is expected to substitute with the corresponding
/// Standard-14 font. Matched after subset-prefix stripping and after the
/// literal Standard-14 names, but before the keyword-based heuristic.
///
/// Aliases are intentionally exact (case-sensitive) matches — the keyword
/// heuristic below already catches free-form variants like "ArialNarrow-Bold".
fn standard_font_alias(name: &str) -> Option<StandardFont> {
    match name {
        // Arial family → Helvetica
        "ArialMT" | "Arial" => Some(StandardFont::Helvetica),
        "Arial-BoldMT" | "Arial,Bold" | "Arial-Bold" => Some(StandardFont::HelveticaBold),
        "Arial-ItalicMT" | "Arial,Italic" | "Arial-Italic" => Some(StandardFont::HelveticaOblique),
        "Arial-BoldItalicMT" | "Arial,BoldItalic" | "Arial-BoldItalic" => {
            Some(StandardFont::HelveticaBoldOblique)
        }
        // Times New Roman family → Times
        "TimesNewRomanPSMT" | "TimesNewRoman" | "TimesNewRomanPS" => Some(StandardFont::TimesRoman),
        "TimesNewRomanPS-BoldMT"
        | "TimesNewRoman-Bold"
        | "TimesNewRomanPS-Bold"
        | "TimesNewRoman,Bold" => Some(StandardFont::TimesBold),
        "TimesNewRomanPS-ItalicMT"
        | "TimesNewRoman-Italic"
        | "TimesNewRomanPS-Italic"
        | "TimesNewRoman,Italic" => Some(StandardFont::TimesItalic),
        "TimesNewRomanPS-BoldItalicMT"
        | "TimesNewRoman-BoldItalic"
        | "TimesNewRomanPS-BoldItalic"
        | "TimesNewRoman,BoldItalic" => Some(StandardFont::TimesBoldItalic),
        // Courier New family → Courier
        "CourierNewPSMT" | "CourierNew" => Some(StandardFont::Courier),
        "CourierNewPS-BoldMT" | "CourierNew-Bold" | "CourierNewPS-Bold" => {
            Some(StandardFont::CourierBold)
        }
        "CourierNewPS-ItalicMT" | "CourierNew-Italic" | "CourierNewPS-Italic" => {
            Some(StandardFont::CourierOblique)
        }
        "CourierNewPS-BoldItalicMT" | "CourierNew-BoldItalic" | "CourierNewPS-BoldItalic" => {
            Some(StandardFont::CourierBoldOblique)
        }
        _ => None,
    }
}

pub(crate) fn select_standard_font(
    dict: &Dict<'_>,
    descriptor: &Dict<'_>,
) -> Option<(StandardFont, bool)> {
    let base_font = dict.get::<Name>(BASE_FONT)?;
    let name = strip_subset_prefix(base_font.as_str());

    // First try whether it matches literally.
    match name {
        "Helvetica" => return Some((StandardFont::Helvetica, true)),
        "Helvetica-Bold" => return Some((StandardFont::HelveticaBold, true)),
        "Helvetica-Oblique" => return Some((StandardFont::HelveticaOblique, true)),
        "Helvetica-BoldOblique" => return Some((StandardFont::HelveticaBoldOblique, true)),
        "Courier" => return Some((StandardFont::Courier, true)),
        "Courier-Bold" => return Some((StandardFont::CourierBold, true)),
        "Courier-Oblique" => return Some((StandardFont::CourierOblique, true)),
        "Courier-BoldOblique" => return Some((StandardFont::CourierBoldOblique, true)),
        "Times-Roman" => return Some((StandardFont::TimesRoman, true)),
        "Times-Bold" => return Some((StandardFont::TimesBold, true)),
        "Times-Italic" => return Some((StandardFont::TimesItalic, true)),
        "Times-BoldItalic" => return Some((StandardFont::TimesBoldItalic, true)),
        "Symbol" => return Some((StandardFont::Symbol, true)),
        "ZapfDingbats" => return Some((StandardFont::ZapfDingBats, true)),
        _ => {}
    }

    // PostScript-name aliases commonly emitted by Office/iText/etc. for
    // unembedded Standard-14-equivalent fonts (e.g. ArialMT → Helvetica).
    // Treated as non-exact so glyph-width fallback in StandardKind still
    // consults the supplied Widths array when present.
    if let Some(alias) = standard_font_alias(name) {
        return Some((alias, false));
    }

    // Now, we bruteforce, trying to determine a suitable font based on the
    // keywords that appear in the name and the descriptor.
    let lower = name.to_ascii_lowercase();

    // FontFamily (descriptor) captures the human-readable family, which is
    // often present even when BaseFont is an opaque subset name. PDF 1.7 §9.8.1
    // specifies it as a text string, but producers in the wild use name
    // objects too; fetch as Name (covers both via implicit conversion).
    let family_field = descriptor
        .get::<Name>(FONT_FAMILY)
        .map(|n| n.as_str().to_ascii_lowercase())
        .unwrap_or_default();

    // PDF spec §9.8.2 Table 120: FontWeight is a number in {100, 200, … 900};
    // 400 is normal and 700 is bold. Adobe considers weights ≥ 600 (SemiBold,
    // DemiBold) as "bold" for substitution purposes — matching that lowers
    // the threshold from 700 to 600 so fonts like "HelveticaNeue-Medium"
    // (weight 500) stay regular but "*-SemiBold" (600) map to the bold face.
    let is_bold = descriptor.get::<u32>(FONT_WEIGHT).is_some_and(|w| w >= 600)
        || lower.contains("bold")
        || lower.contains("demi")
        || family_field.contains("bold")
        || family_field.contains("demi");
    // PDF spec §9.8.2 Table 120: ItalicAngle is the angle, in counter-clockwise
    // degrees, of the dominant vertical strokes. Italic/oblique faces are
    // negative (typically -10° to -20°). Previously we accepted any non-zero
    // value, which mis-classified upright fonts that shipped with tiny
    // rounding noise (e.g. -0.1). The stricter −5° threshold follows what
    // PDF.js and PDFBox use and avoids that false-positive.
    let is_italic = descriptor
        .get::<f32>(ITALIC_ANGLE)
        .is_some_and(|a| !(-5.0..=5.0).contains(&a))
        || lower.contains("italic")
        || lower.contains("oblique")
        || family_field.contains("italic")
        || family_field.contains("oblique");

    // Keyword/family heuristic. Prefer BaseFont; fall back to FontFamily.
    let haystack = if family_field.is_empty() {
        lower.clone()
    } else {
        format!("{lower} {family_field}")
    };

    // Keyword/family heuristic (last resort — only reached when the font name
    // did not match any Standard-14 alias above).
    //
    // `exact` controls whether the caller should trust PDF /Widths entries or
    // fall back to Standard-14 AFM metrics:
    //   exact=true  → AFM metrics used; PDF /Widths ignored (safe for genuine
    //                 Standard-14 faces whose names survived case folding here)
    //   exact=false → PDF /Widths respected when present; AFM is only fallback
    //                 (correct for non-Standard-14 lookalikes like "ArialMT")
    //
    // GL-QA38 regression note: setting exact=false for ALL heuristic matches
    // caused 116 SSIM regressions in gate-5k-04 because many PDFs that contained
    // "helvetica" or "times" in the font name ARE genuine Standard-14 and their
    // /Widths arrays (when present) are less accurate than Standard-14 AFM.
    // The corrected approach: treat Standard-14 keyword matches (helvetica,
    // courier, times) as exact=true; treat clear non-Standard-14 keywords
    // (arial, sans, mono, serif without "times") as exact=false so we respect
    // their embedded /Widths.
    let (family, exact) = if haystack.contains("helvetica") {
        (Some(StandardFontFamily::Helvetica), true) // likely genuine Helvetica — use AFM
    } else if haystack.contains("arial") || haystack.contains("sans") {
        (Some(StandardFontFamily::Helvetica), false) // Arial/generic sans — respect /Widths
    } else if haystack.contains("courier") {
        (Some(StandardFontFamily::Courier), true) // likely genuine Courier — use AFM
    } else if haystack.contains("mono") {
        (Some(StandardFontFamily::Courier), false) // generic monospace — respect /Widths
    } else if haystack.contains("times") {
        (Some(StandardFontFamily::Times), true) // likely genuine Times — use AFM
    } else if haystack.contains("serif") {
        (Some(StandardFontFamily::Times), false) // generic serif — respect /Widths
    } else if haystack.contains("zapfdingbats") || haystack.contains("dingbats") {
        return Some((StandardFont::ZapfDingBats, false));
    } else {
        (None, false)
    };

    let font = match (family?, is_bold, is_italic) {
        (StandardFontFamily::Helvetica, false, false) => StandardFont::Helvetica,
        (StandardFontFamily::Helvetica, true, false) => StandardFont::HelveticaBold,
        (StandardFontFamily::Helvetica, false, true) => StandardFont::HelveticaOblique,
        (StandardFontFamily::Helvetica, true, true) => StandardFont::HelveticaBoldOblique,
        (StandardFontFamily::Courier, false, false) => StandardFont::Courier,
        (StandardFontFamily::Courier, true, false) => StandardFont::CourierBold,
        (StandardFontFamily::Courier, false, true) => StandardFont::CourierOblique,
        (StandardFontFamily::Courier, true, true) => StandardFont::CourierBoldOblique,
        (StandardFontFamily::Times, false, false) => StandardFont::TimesRoman,
        (StandardFontFamily::Times, true, false) => StandardFont::TimesBold,
        (StandardFontFamily::Times, false, true) => StandardFont::TimesItalic,
        (StandardFontFamily::Times, true, true) => StandardFont::TimesBoldItalic,
    };

    Some((font, exact))
}

#[derive(Debug)]
pub(crate) enum StandardFontBlob {
    Cff(CffFontBlob),
    Otf(OpenTypeFontBlob, HashMap<String, GlyphId>),
}

impl StandardFontBlob {
    pub(crate) fn from_data(data: FontData, index: u32) -> Option<Self> {
        if let Some(blob) = CffFontBlob::new(data.clone()) {
            Some(Self::new_cff(blob))
        } else {
            OpenTypeFontBlob::new(data, index).map(Self::new_otf)
        }
    }

    pub(crate) fn new_cff(blob: CffFontBlob) -> Self {
        Self::Cff(blob)
    }

    pub(crate) fn new_otf(blob: OpenTypeFontBlob) -> Self {
        let mut glyph_names = HashMap::new();

        if let Ok(post) = blob.font_ref().post() {
            for i in 0..blob.num_glyphs() {
                if let Some(str) = post.glyph_name(GlyphId16::new(i)) {
                    glyph_names.insert(str.to_string(), GlyphId::new(i as u32));
                }
            }
        }

        Self::Otf(blob, glyph_names)
    }
}

impl StandardFontBlob {
    pub(crate) fn name_to_glyph(&self, name: &str) -> Option<GlyphId> {
        match self {
            Self::Cff(blob) => blob
                .table()
                .glyph_index_by_name(name)
                .map(|g| GlyphId::new(g.0 as u32)),
            Self::Otf(_, glyph_names) => glyph_names.get(name).copied(),
        }
    }

    pub(crate) fn unicode_to_glyph(&self, code: u32) -> Option<GlyphId> {
        match self {
            Self::Cff(_) => None,
            Self::Otf(blob, _) => blob
                .font_ref()
                .cmap()
                .ok()
                .and_then(|c| c.map_codepoint(code)),
        }
    }

    pub(crate) fn advance_width(&self, glyph: GlyphId) -> Option<f32> {
        match self {
            Self::Cff(_) => None,
            Self::Otf(blob, _) => blob.glyph_metrics().advance_width(glyph),
        }
    }

    pub(crate) fn outline_glyph(&self, glyph: GlyphId) -> BezPath {
        // Standard fonts have empty outlines for these, but in Liberation Sans
        // they are a .notdef rectangle.
        if glyph == GlyphId::NOTDEF {
            return BezPath::new();
        }

        match self {
            Self::Cff(blob) => blob.outline_glyph(glyph),
            Self::Otf(blob, _) => blob.outline_glyph(glyph),
        }
    }
}

#[derive(Debug)]
pub(crate) struct StandardKind {
    base_font: StandardFont,
    base_font_blob: StandardFontBlob,
    encoding: Encoding,
    widths: Vec<Width>,
    missing_width: Option<f32>,
    fallback: bool,
    glyph_to_code: RefCell<HashMap<GlyphId, u8>>,
    encodings: HashMap<u8, String>,
}

impl StandardKind {
    pub(crate) fn new(dict: &Dict<'_>, resolver: &FontResolverFn) -> Option<Self> {
        let descriptor = dict.get::<Dict<'_>>(FONT_DESC).unwrap_or_default();
        let (font, exact) = select_standard_font(dict, &descriptor)?;
        Self::new_with_standard(dict, font, !exact, resolver)
    }

    pub(crate) fn new_with_standard(
        dict: &Dict<'_>,
        base_font: StandardFont,
        fallback: bool,
        resolver: &FontResolverFn,
    ) -> Option<Self> {
        let descriptor = dict.get::<Dict<'_>>(FONT_DESC).unwrap_or_default();
        let (widths, missing_width) = read_widths(dict, &descriptor)?;
        let missing_width = descriptor
            .contains_key(MISSING_WIDTH)
            .then_some(missing_width);

        let (mut encoding, encoding_map) = read_encoding(dict);

        // See PDFJS-16464: Ignore encodings for non-embedded Type1 symbol fonts.
        if matches!(base_font, StandardFont::Symbol | StandardFont::ZapfDingBats) {
            encoding = Encoding::BuiltIn;
        }

        let (blob, index) = resolver(&FontQuery::Standard(base_font))?;
        let base_font_blob = StandardFontBlob::from_data(blob, index)?;

        Some(Self {
            base_font,
            base_font_blob,
            widths,
            missing_width,
            encodings: encoding_map,
            glyph_to_code: RefCell::new(HashMap::new()),
            fallback,
            encoding,
        })
    }

    fn code_to_ps_name(&self, code: u8) -> Option<&str> {
        let bf = self.base_font;

        self.encodings
            .get(&code)
            .map(String::as_str)
            .or_else(|| match self.encoding {
                Encoding::BuiltIn => bf.code_to_name(code),
                _ => self.encoding.map_code(code),
            })
    }

    pub(crate) fn map_code(&self, code: u8) -> GlyphId {
        let result = self
            .code_to_ps_name(code)
            .and_then(|c| {
                self.base_font_blob.name_to_glyph(c).or_else(|| {
                    // If the font doesn't have a POST table, try to map via unicode instead.
                    glyph_names::get(c).and_then(|c| {
                        self.base_font_blob
                            .unicode_to_glyph(c.chars().nth(0).unwrap() as u32)
                    })
                })
            })
            .unwrap_or(GlyphId::NOTDEF);
        self.glyph_to_code.borrow_mut().insert(result, code);

        result
    }

    pub(crate) fn outline_glyph(&self, glyph: GlyphId) -> BezPath {
        let path = self.base_font_blob.outline_glyph(glyph);

        // If the font is not embedded, we might need to stretch it so that
        // it matches the metrics of the actual underlying font blob.

        if let Some(code) = self.glyph_to_code.borrow().get(&glyph).copied()
            && let Some(actual_width) = self.base_font_blob.advance_width(glyph).or_else(|| {
                self.code_to_ps_name(code)
                    .and_then(|name| self.base_font.get_width(name))
            })
        {
            // From my experiments: Most PDF viewers, if they detect a font is a
            // standard font, they completely ignore the widths array, even if
            // different widths are indicated there. So only if it's an unknown
            // font do we check the widths array. Otherwise, we always use the
            // base font metrics.
            let should_width = if self.fallback {
                if let Some(Width::Value(w)) = self.widths.get(code as usize).copied() {
                    w
                } else {
                    return path;
                }
            } else if let Some(w) = self
                .code_to_ps_name(code)
                .and_then(|name| self.base_font.get_width(name))
            {
                w
            } else {
                return path;
            };

            return stretch_glyph(path, should_width, actual_width);
        }

        path
    }

    pub(crate) fn glyph_width(&self, code: u8) -> Option<f32> {
        match self.widths.get(code as usize).copied() {
            Some(Width::Value(w)) => Some(w),
            Some(Width::Missing) => self.missing_width.or_else(|| {
                self.code_to_ps_name(code)
                    .and_then(|c| self.base_font.get_width(c))
            }),
            _ => self
                .code_to_ps_name(code)
                .and_then(|c| self.base_font.get_width(c)),
        }
    }

    pub(crate) fn char_code_to_unicode(&self, code: u8) -> Option<char> {
        self.code_to_ps_name(code).and_then(glyph_name_to_unicode)
    }

    pub(crate) fn is_italic(&self) -> bool {
        self.base_font.is_italic()
    }

    pub(crate) fn is_bold(&self) -> bool {
        self.base_font.is_bold()
    }

    pub(crate) fn is_serif(&self) -> bool {
        self.base_font.is_serif()
    }

    pub(crate) fn is_monospace(&self) -> bool {
        self.base_font.is_monospace()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn build_widths(entries: &[(u8, f32)]) -> Vec<Width> {
        let mut widths = vec![Width::Missing; 256];
        for (code, width) in entries {
            widths[*code as usize] = Width::Value(*width);
        }
        widths
    }

    fn build_standard_kind(widths: Vec<Width>, missing_width: Option<f32>) -> StandardKind {
        let (data, index) = StandardFont::Helvetica.get_font_data();
        let base_font_blob =
            StandardFontBlob::from_data(data, index).expect("standard font data should parse");

        StandardKind {
            base_font: StandardFont::Helvetica,
            base_font_blob,
            encoding: Encoding::WinAnsi,
            widths,
            missing_width,
            fallback: true,
            glyph_to_code: RefCell::new(HashMap::new()),
            encodings: HashMap::new(),
        }
    }

    #[test]
    fn glyph_width_falls_back_to_base_metrics_when_missing_width_is_absent() {
        let font = build_standard_kind(build_widths(&[(b'A', 600.0)]), None);

        assert_eq!(font.glyph_width(b'A'), Some(600.0));
        assert_eq!(
            font.glyph_width(b'B'),
            StandardFont::Helvetica.get_width("B")
        );
    }

    #[test]
    fn glyph_width_respects_explicit_zero_missing_width() {
        let font = build_standard_kind(build_widths(&[(b'A', 600.0)]), Some(0.0));

        assert_eq!(font.glyph_width(b'A'), Some(600.0));
        assert_eq!(font.glyph_width(b'B'), Some(0.0));
    }

    #[test]
    fn arial_aliases_resolve_to_helvetica_family() {
        assert!(matches!(
            standard_font_alias("ArialMT"),
            Some(StandardFont::Helvetica)
        ));
        assert!(matches!(
            standard_font_alias("Arial-BoldMT"),
            Some(StandardFont::HelveticaBold)
        ));
        assert!(matches!(
            standard_font_alias("Arial-ItalicMT"),
            Some(StandardFont::HelveticaOblique)
        ));
        assert!(matches!(
            standard_font_alias("Arial-BoldItalicMT"),
            Some(StandardFont::HelveticaBoldOblique)
        ));
    }

    #[test]
    fn times_new_roman_aliases_resolve_to_times_family() {
        assert!(matches!(
            standard_font_alias("TimesNewRomanPSMT"),
            Some(StandardFont::TimesRoman)
        ));
        assert!(matches!(
            standard_font_alias("TimesNewRomanPS-BoldMT"),
            Some(StandardFont::TimesBold)
        ));
        assert!(matches!(
            standard_font_alias("TimesNewRomanPS-ItalicMT"),
            Some(StandardFont::TimesItalic)
        ));
        assert!(matches!(
            standard_font_alias("TimesNewRomanPS-BoldItalicMT"),
            Some(StandardFont::TimesBoldItalic)
        ));
    }

    #[test]
    fn courier_new_aliases_resolve_to_courier_family() {
        assert!(matches!(
            standard_font_alias("CourierNewPSMT"),
            Some(StandardFont::Courier)
        ));
        assert!(matches!(
            standard_font_alias("CourierNewPS-BoldMT"),
            Some(StandardFont::CourierBold)
        ));
        assert!(matches!(
            standard_font_alias("CourierNewPS-ItalicMT"),
            Some(StandardFont::CourierOblique)
        ));
        assert!(matches!(
            standard_font_alias("CourierNewPS-BoldItalicMT"),
            Some(StandardFont::CourierBoldOblique)
        ));
    }

    #[test]
    fn unknown_names_do_not_alias() {
        assert!(standard_font_alias("LiberationSans").is_none());
        assert!(standard_font_alias("CenturySchoolbook").is_none());
        assert!(standard_font_alias("").is_none());
    }
}
