//! Shaping support for autohinting.

use super::style::{GlyphStyle, StyleClass};
use crate::{charmap::Charmap, collections::SmallVec, FontRef, GlyphId, MetadataProvider};
use core::ops::Range;
use raw::{
    tables::{
        gsub::{
            ChainedSequenceContext, Gsub, SequenceContext, SingleSubst, SubstitutionLookupList,
            SubstitutionSubtables,
        },
        layout::{Feature, ScriptTags},
        varc::CoverageTable,
    },
    types::Tag,
    ReadError, TableProvider,
};

// To prevent infinite recursion in contextual lookups. Matches HB
// <https://github.com/harfbuzz/harfbuzz/blob/c7ef6a2ed58ae8ec108ee0962bef46f42c73a60c/src/hb-limits.hh#L53>
const MAX_NESTING_DEPTH: usize = 64;

/// Determines the fidelity with which we apply shaping in the
/// autohinter.
///
/// Shaping only affects glyph style classification and the glyphs that
/// are chosen for metrics computations. We keep the `Nominal` mode around
/// to enable validation of internal algorithms against a configuration that
/// is known to match FreeType. The `BestEffort` mode should always be
/// used for actual rendering.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub(crate) enum ShaperMode {
    /// Characters are mapped to nominal glyph identifiers and layout tables
    /// are not used for style coverage.
    ///
    /// This matches FreeType when HarfBuzz support is not enabled.
    Nominal,
    /// Simple substitutions are applied according to script rules and layout
    /// tables are used to extend style coverage beyond the character map.
    #[allow(unused)]
    BestEffort,
}

#[derive(Copy, Clone, Default, Debug)]
pub(crate) struct ShapedGlyph {
    pub id: GlyphId,
    /// This may be used for computing vertical alignment zones, particularly
    /// for glyphs like super/subscripts which might have adjustments in GPOS.
    ///
    /// Note that we don't do the same in the horizontal direction which
    /// means that we don't care about the x-offset.
    pub y_offset: i32,
}

/// Arbitrarily chosen to cover our max input size plus some extra to account
/// for expansion from multiple substitution tables.
const SHAPED_CLUSTER_INLINE_SIZE: usize = 16;

/// Container for storing the result of shaping a cluster.
///
/// Some of our input "characters" for metrics computations are actually
/// multi-character [grapheme clusters](https://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries)
/// that may expand to multiple glyphs.
pub(crate) type ShapedCluster = SmallVec<ShapedGlyph, SHAPED_CLUSTER_INLINE_SIZE>;

#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub(crate) enum ShaperCoverageKind {
    /// Shaper coverage that traverses a specific script.
    Script,
    /// Shaper coverage that also includes the `Dflt` script.
    ///
    /// This is used as a catch all after all styles are processed.
    Default,
}

/// Maps characters to glyphs and handles extended style coverage beyond
/// glyphs that are available in the character map.
///
/// Roughly covers the functionality in <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c>.
pub(crate) struct Shaper<'a> {
    font: FontRef<'a>,
    #[allow(unused)]
    mode: ShaperMode,
    charmap: Charmap<'a>,
    gsub: Option<Gsub<'a>>,
}

impl<'a> Shaper<'a> {
    pub fn new(font: &FontRef<'a>, mode: ShaperMode) -> Self {
        let charmap = font.charmap();
        let gsub = (mode != ShaperMode::Nominal)
            .then(|| font.gsub().ok())
            .flatten();
        Self {
            font: font.clone(),
            mode,
            charmap,
            gsub,
        }
    }

    pub fn font(&self) -> &FontRef<'a> {
        &self.font
    }

    pub fn charmap(&self) -> &Charmap<'a> {
        &self.charmap
    }

    pub fn lookup_count(&self) -> u16 {
        self.gsub
            .as_ref()
            .and_then(|gsub| gsub.lookup_list().ok())
            .map(|list| list.lookup_count())
            .unwrap_or_default()
    }

    pub fn cluster_shaper(&'a self, style: &StyleClass) -> ClusterShaper<'a> {
        if self.mode == ShaperMode::BestEffort {
            // For now, only apply substitutions for styles with an associated
            // feature
            if let Some(feature_tag) = style.feature {
                if let Some((lookup_list, feature)) = self.gsub.as_ref().and_then(|gsub| {
                    let script_list = gsub.script_list().ok()?;
                    let selected_script =
                        script_list.select(&ScriptTags::from_unicode(style.script.tag))?;
                    let script = script_list.get(selected_script.index).ok()?;
                    let lang_sys = script.default_lang_sys()?.ok()?;
                    let feature_list = gsub.feature_list().ok()?;
                    let feature_ix = lang_sys.feature_index_for_tag(&feature_list, feature_tag)?;
                    let feature = feature_list.get(feature_ix).ok()?.element;
                    let lookup_list = gsub.lookup_list().ok()?;
                    Some((lookup_list, feature))
                }) {
                    return ClusterShaper {
                        shaper: self,
                        lookup_list: Some(lookup_list),
                        kind: ClusterShaperKind::SingleFeature(feature),
                    };
                }
            }
        }
        ClusterShaper {
            shaper: self,
            lookup_list: None,
            kind: ClusterShaperKind::Nominal,
        }
    }

    /// Uses layout tables to compute coverage for the given style.
    ///
    /// Returns `true` if any glyph styles were updated for this style.
    ///
    /// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L99>
    pub(crate) fn compute_coverage(
        &self,
        style: &StyleClass,
        coverage_kind: ShaperCoverageKind,
        glyph_styles: &mut [GlyphStyle],
        visited_set: &mut VisitedLookupSet<'_>,
    ) -> bool {
        let Some(gsub) = self.gsub.as_ref() else {
            return false;
        };
        let (Ok(script_list), Ok(feature_list), Ok(lookup_list)) =
            (gsub.script_list(), gsub.feature_list(), gsub.lookup_list())
        else {
            return false;
        };
        let mut script_tags: [Option<Tag>; 3] = [None; 3];
        for (a, b) in script_tags
            .iter_mut()
            .zip(ScriptTags::from_unicode(style.script.tag).iter())
        {
            *a = Some(*b);
        }
        // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L153>
        const DEFAULT_SCRIPT: Tag = Tag::new(b"DFLT");
        if coverage_kind == ShaperCoverageKind::Default {
            if script_tags[0].is_none() {
                script_tags[0] = Some(DEFAULT_SCRIPT);
            } else if script_tags[1].is_none() {
                script_tags[1] = Some(DEFAULT_SCRIPT);
            } else if script_tags[1] != Some(DEFAULT_SCRIPT) {
                script_tags[2] = Some(DEFAULT_SCRIPT);
            }
        } else {
            // Script classes contain some non-standard tags used for special
            // purposes. We ignore these
            // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L167>
            const NON_STANDARD_TAGS: &[Option<Tag>] = &[
                // Khmer symbols
                Some(Tag::new(b"Khms")),
                // Latin subscript fallbacks
                Some(Tag::new(b"Latb")),
                // Latin superscript fallbacks
                Some(Tag::new(b"Latp")),
            ];
            if NON_STANDARD_TAGS.contains(&script_tags[0]) {
                return false;
            }
        }
        // Check each requested script that is available in GSUB
        let mut gsub_handler = GsubHandler::new(
            &self.charmap,
            &lookup_list,
            style,
            glyph_styles,
            visited_set,
        );
        for script in script_tags.iter().filter_map(|tag| {
            tag.and_then(|tag| script_list.index_for_tag(tag))
                .and_then(|ix| script_list.script_records().get(ix as usize))
                .and_then(|rec| rec.script(script_list.offset_data()).ok())
        }) {
            // And all language systems for each script
            for langsys in script
                .lang_sys_records()
                .iter()
                .filter_map(|rec| rec.lang_sys(script.offset_data()).ok())
                .chain(script.default_lang_sys().transpose().ok().flatten())
            {
                for feature_ix in langsys.feature_indices() {
                    let Some(feature) = feature_list
                        .feature_records()
                        .get(feature_ix.get() as usize)
                        .and_then(|rec| {
                            // If our style has a feature tag, we only look at that specific
                            // feature; otherwise, handle all of them
                            if style.feature == Some(rec.feature_tag()) || style.feature.is_none() {
                                rec.feature(feature_list.offset_data()).ok()
                            } else {
                                None
                            }
                        })
                    else {
                        continue;
                    };
                    // And now process associated lookups
                    for index in feature.lookup_list_indices().iter() {
                        // We only care about errors here for testing
                        let _ = gsub_handler.process_lookup(index.get());
                    }
                }
            }
        }
        if let Some(range) = gsub_handler.finish() {
            // If we get a range then we captured at least some glyphs so
            // let's try to assign our current style
            let mut result = false;
            for glyph_style in &mut glyph_styles[range] {
                // We only want to return true here if we actually assign the
                // style to avoid computing unnecessary metrics
                result |= glyph_style.maybe_assign_gsub_output_style(style);
            }
            result
        } else {
            false
        }
    }
}

pub(crate) struct ClusterShaper<'a> {
    shaper: &'a Shaper<'a>,
    lookup_list: Option<SubstitutionLookupList<'a>>,
    kind: ClusterShaperKind<'a>,
}

impl ClusterShaper<'_> {
    pub(crate) fn shape(&mut self, input: &str, output: &mut ShapedCluster) {
        // First fill the output cluster with the nominal character
        // to glyph id mapping
        output.clear();
        for ch in input.chars() {
            output.push(ShapedGlyph {
                id: self.shaper.charmap.map(ch).unwrap_or_default(),
                y_offset: 0,
            });
        }
        match self.kind.clone() {
            ClusterShaperKind::Nominal => {
                // In nominal mode, reject clusters with multiple glyphs
                // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L639>
                if self.shaper.mode == ShaperMode::Nominal && output.len() > 1 {
                    output.clear();
                }
            }
            ClusterShaperKind::SingleFeature(feature) => {
                let mut did_subst = false;
                for lookup_ix in feature.lookup_list_indices() {
                    let mut glyph_ix = 0;
                    while glyph_ix < output.len() {
                        did_subst |= self.apply_lookup(lookup_ix.get(), output, glyph_ix, 0);
                        glyph_ix += 1;
                    }
                }
                // Reject clusters that weren't modified by the feature.
                // FreeType detects this by shaping twice and comparing gids
                // but we just track substitutions
                // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L528>
                if !did_subst {
                    output.clear();
                }
            }
        }
    }

    fn apply_lookup(
        &self,
        lookup_index: u16,
        cluster: &mut ShapedCluster,
        glyph_ix: usize,
        nesting_depth: usize,
    ) -> bool {
        if nesting_depth > MAX_NESTING_DEPTH {
            return false;
        }
        let Some(glyph) = cluster.get_mut(glyph_ix) else {
            return false;
        };
        let Some(subtables) = self
            .lookup_list
            .as_ref()
            .and_then(|list| list.lookups().get(lookup_index as usize).ok())
            .and_then(|lookup| lookup.subtables().ok())
        else {
            return false;
        };
        match subtables {
            // For now, just applying single substitutions because we're
            // currently only handling shaping for "feature" styles like
            // c2sc (caps to small caps) which are (almost?) always
            // single substs
            SubstitutionSubtables::Single(tables) => {
                for table in tables.iter().filter_map(|table| table.ok()) {
                    match table {
                        SingleSubst::Format1(table) => {
                            let Some(_) = table.coverage().ok().and_then(|cov| cov.get(glyph.id))
                            else {
                                continue;
                            };
                            let delta = table.delta_glyph_id() as i32;
                            glyph.id = GlyphId::from((glyph.id.to_u32() as i32 + delta) as u16);
                            return true;
                        }
                        SingleSubst::Format2(table) => {
                            let Some(cov_ix) =
                                table.coverage().ok().and_then(|cov| cov.get(glyph.id))
                            else {
                                continue;
                            };
                            let Some(subst) = table.substitute_glyph_ids().get(cov_ix as usize)
                            else {
                                continue;
                            };
                            glyph.id = subst.get().into();
                            return true;
                        }
                    }
                }
            }
            SubstitutionSubtables::Multiple(_tables) => {}
            SubstitutionSubtables::Ligature(_tables) => {}
            SubstitutionSubtables::Alternate(_tables) => {}
            SubstitutionSubtables::Contextual(_tables) => {}
            SubstitutionSubtables::ChainContextual(_tables) => {}
            SubstitutionSubtables::Reverse(_tables) => {}
        }
        false
    }
}

#[derive(Clone)]
enum ClusterShaperKind<'a> {
    Nominal,
    SingleFeature(Feature<'a>),
}

/// Captures glyphs from the GSUB table that aren't present in cmap.
///
/// FreeType does this in a few phases:
/// 1. Collect all lookups for a given set of scripts and features.
///    <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L174>
/// 2. For each lookup, collect all _output_ glyphs.
///    <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L201>
/// 3. If the style represents a specific feature, make sure at least one of
///    the characters in the associated blue string would be substituted by
///    those lookups. If none would be substituted, then we don't assign the
///    style to any glyphs because we don't have any modified alignment
///    zones.
///    <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L264>
///
/// We roll these into one pass over the lookups below so that we don't have
/// to allocate a lookup set or iterate them twice. Note that since
/// substitutions are checked for individual characters, we ignore ligatures
/// and contextual lookups (and alternates since they aren't applicable).
struct GsubHandler<'a, 'b> {
    charmap: &'a Charmap<'a>,
    lookup_list: &'a SubstitutionLookupList<'a>,
    style: &'a StyleClass,
    glyph_styles: &'a mut [GlyphStyle],
    // Set to true when we need to check if any substitutions are available
    // for our blue strings. This is the case when style.feature != None
    need_blue_substs: bool,
    // Keep track of our range of touched gids in the style list
    min_gid: usize,
    max_gid: usize,
    lookup_depth: usize,
    visited_set: &'a mut VisitedLookupSet<'b>,
}

impl<'a, 'b> GsubHandler<'a, 'b> {
    fn new(
        charmap: &'a Charmap<'a>,
        lookup_list: &'a SubstitutionLookupList,
        style: &'a StyleClass,
        glyph_styles: &'a mut [GlyphStyle],
        visited_set: &'a mut VisitedLookupSet<'b>,
    ) -> Self {
        let min_gid = glyph_styles.len();
        // If we have a feature, then we need to check the blue string to see
        // if any substitutions are available. If not, we don't enable this
        // style because it won't have any affect on alignment zones
        let need_blue_substs = style.feature.is_some();
        Self {
            charmap,
            lookup_list,
            style,
            glyph_styles,
            need_blue_substs,
            min_gid,
            max_gid: 0,
            lookup_depth: 0,
            visited_set,
        }
    }

    fn process_lookup(&mut self, lookup_index: u16) -> Result<(), ProcessLookupError> {
        // General protection against stack overflows
        if self.lookup_depth == MAX_NESTING_DEPTH {
            return Err(ProcessLookupError::ExceededMaxDepth);
        }
        // Skip lookups that have already been processed
        if !self.visited_set.insert(lookup_index) {
            return Ok(());
        }
        self.lookup_depth += 1;
        // Actually process the lookup
        let result = self.process_lookup_inner(lookup_index);
        // Out we go again
        self.lookup_depth -= 1;
        result
    }

    #[inline(always)]
    fn process_lookup_inner(&mut self, lookup_index: u16) -> Result<(), ProcessLookupError> {
        let Ok(subtables) = self
            .lookup_list
            .lookups()
            .get(lookup_index as usize)
            .and_then(|lookup| lookup.subtables())
        else {
            return Ok(());
        };
        match subtables {
            SubstitutionSubtables::Single(tables) => {
                for table in tables.iter().filter_map(|table| table.ok()) {
                    match table {
                        SingleSubst::Format1(table) => {
                            let Ok(coverage) = table.coverage() else {
                                continue;
                            };
                            let delta = table.delta_glyph_id() as i32;
                            for gid in coverage.iter() {
                                self.capture_glyph((gid.to_u32() as i32 + delta) as u16 as u32);
                            }
                            // Check input coverage for blue strings if
                            // required and if we're not under a contextual
                            // lookup
                            if self.need_blue_substs && self.lookup_depth == 1 {
                                self.check_blue_coverage(Ok(coverage));
                            }
                        }
                        SingleSubst::Format2(table) => {
                            for gid in table.substitute_glyph_ids() {
                                self.capture_glyph(gid.get().to_u32());
                            }
                            // See above
                            if self.need_blue_substs && self.lookup_depth == 1 {
                                self.check_blue_coverage(table.coverage());
                            }
                        }
                    }
                }
            }
            SubstitutionSubtables::Multiple(tables) => {
                for table in tables.iter().filter_map(|table| table.ok()) {
                    for seq in table.sequences().iter().filter_map(|seq| seq.ok()) {
                        for gid in seq.substitute_glyph_ids() {
                            self.capture_glyph(gid.get().to_u32());
                        }
                    }
                    // See above
                    if self.need_blue_substs && self.lookup_depth == 1 {
                        self.check_blue_coverage(table.coverage());
                    }
                }
            }
            SubstitutionSubtables::Ligature(tables) => {
                for table in tables.iter().filter_map(|table| table.ok()) {
                    for set in table.ligature_sets().iter().filter_map(|set| set.ok()) {
                        for lig in set.ligatures().iter().filter_map(|lig| lig.ok()) {
                            self.capture_glyph(lig.ligature_glyph().to_u32());
                        }
                    }
                }
            }
            SubstitutionSubtables::Alternate(tables) => {
                for table in tables.iter().filter_map(|table| table.ok()) {
                    for set in table.alternate_sets().iter().filter_map(|set| set.ok()) {
                        for gid in set.alternate_glyph_ids() {
                            self.capture_glyph(gid.get().to_u32());
                        }
                    }
                }
            }
            SubstitutionSubtables::Contextual(tables) => {
                for table in tables.iter().filter_map(|table| table.ok()) {
                    match table {
                        SequenceContext::Format1(table) => {
                            for set in table
                                .seq_rule_sets()
                                .iter()
                                .filter_map(|set| set.transpose().ok().flatten())
                            {
                                for rule in set.seq_rules().iter().filter_map(|rule| rule.ok()) {
                                    for rec in rule.seq_lookup_records() {
                                        self.process_lookup(rec.lookup_list_index())?;
                                    }
                                }
                            }
                        }
                        SequenceContext::Format2(table) => {
                            for set in table
                                .class_seq_rule_sets()
                                .iter()
                                .filter_map(|set| set.transpose().ok().flatten())
                            {
                                for rule in
                                    set.class_seq_rules().iter().filter_map(|rule| rule.ok())
                                {
                                    for rec in rule.seq_lookup_records() {
                                        self.process_lookup(rec.lookup_list_index())?;
                                    }
                                }
                            }
                        }
                        SequenceContext::Format3(table) => {
                            for rec in table.seq_lookup_records() {
                                self.process_lookup(rec.lookup_list_index())?;
                            }
                        }
                    }
                }
            }
            SubstitutionSubtables::ChainContextual(tables) => {
                for table in tables.iter().filter_map(|table| table.ok()) {
                    match table {
                        ChainedSequenceContext::Format1(table) => {
                            for set in table
                                .chained_seq_rule_sets()
                                .iter()
                                .filter_map(|set| set.transpose().ok().flatten())
                            {
                                for rule in
                                    set.chained_seq_rules().iter().filter_map(|rule| rule.ok())
                                {
                                    for rec in rule.seq_lookup_records() {
                                        self.process_lookup(rec.lookup_list_index())?;
                                    }
                                }
                            }
                        }
                        ChainedSequenceContext::Format2(table) => {
                            for set in table
                                .chained_class_seq_rule_sets()
                                .iter()
                                .filter_map(|set| set.transpose().ok().flatten())
                            {
                                for rule in set
                                    .chained_class_seq_rules()
                                    .iter()
                                    .filter_map(|rule| rule.ok())
                                {
                                    for rec in rule.seq_lookup_records() {
                                        self.process_lookup(rec.lookup_list_index())?;
                                    }
                                }
                            }
                        }
                        ChainedSequenceContext::Format3(table) => {
                            for rec in table.seq_lookup_records() {
                                self.process_lookup(rec.lookup_list_index())?;
                            }
                        }
                    }
                }
            }
            SubstitutionSubtables::Reverse(tables) => {
                for table in tables.iter().filter_map(|table| table.ok()) {
                    for gid in table.substitute_glyph_ids() {
                        self.capture_glyph(gid.get().to_u32());
                    }
                }
            }
        }
        Ok(())
    }

    /// Finishes processing for this set of GSUB lookups and
    /// returns the range of touched glyphs.
    fn finish(self) -> Option<Range<usize>> {
        self.visited_set.clear();
        if self.min_gid > self.max_gid {
            // We didn't touch any glyphs
            return None;
        }
        let range = self.min_gid..self.max_gid + 1;
        if self.need_blue_substs {
            // We didn't find any substitutions for our blue strings so
            // we ignore the style. Clear the GSUB marker for any touched
            // glyphs
            for glyph in &mut self.glyph_styles[range] {
                glyph.clear_from_gsub();
            }
            None
        } else {
            Some(range)
        }
    }

    /// Checks the given coverage table for any characters in the blue
    /// strings associated with our current style.
    fn check_blue_coverage(&mut self, coverage: Result<CoverageTable<'a>, ReadError>) {
        let Ok(coverage) = coverage else {
            return;
        };
        for (blue_str, _) in self.style.script.blues {
            if blue_str
                .chars()
                .filter_map(|ch| self.charmap.map(ch))
                .filter_map(|gid| coverage.get(gid))
                .next()
                .is_some()
            {
                // Condition satisfied, so don't check any further subtables
                self.need_blue_substs = false;
                return;
            }
        }
    }

    fn capture_glyph(&mut self, gid: u32) {
        let gid = gid as usize;
        if let Some(style) = self.glyph_styles.get_mut(gid) {
            style.set_from_gsub_output();
            self.min_gid = gid.min(self.min_gid);
            self.max_gid = gid.max(self.max_gid);
        }
    }
}

pub(crate) struct VisitedLookupSet<'a>(&'a mut [u8]);

impl<'a> VisitedLookupSet<'a> {
    pub fn new(storage: &'a mut [u8]) -> Self {
        Self(storage)
    }

    /// If the given lookup index is not already in the set, adds it and
    /// returns `true`. Returns `false` otherwise.
    ///
    /// This follows the behavior of `HashSet::insert`.
    fn insert(&mut self, lookup_index: u16) -> bool {
        let byte_ix = lookup_index as usize / 8;
        let bit_mask = 1 << (lookup_index % 8) as u8;
        if let Some(byte) = self.0.get_mut(byte_ix) {
            if *byte & bit_mask == 0 {
                *byte |= bit_mask;
                true
            } else {
                false
            }
        } else {
            false
        }
    }

    fn clear(&mut self) {
        self.0.fill(0);
    }
}

#[derive(PartialEq, Debug)]
enum ProcessLookupError {
    ExceededMaxDepth,
}

#[cfg(test)]
mod tests {
    use super::{super::style, *};
    use font_test_data::bebuffer::BeBuffer;
    use raw::{FontData, FontRead};

    #[test]
    fn small_caps_subst() {
        let font = FontRef::new(font_test_data::NOTOSERIF_AUTOHINT_SHAPING).unwrap();
        let shaper = Shaper::new(&font, ShaperMode::BestEffort);
        let style = &style::STYLE_CLASSES[style::StyleClass::LATN_C2SC];
        let mut cluster_shaper = shaper.cluster_shaper(style);
        let mut cluster = ShapedCluster::new();
        cluster_shaper.shape("H", &mut cluster);
        assert_eq!(cluster.len(), 1);
        // from ttx, gid 8 is small caps "H"
        assert_eq!(cluster[0].id, GlyphId::new(8));
    }

    #[test]
    fn small_caps_nominal() {
        let font = FontRef::new(font_test_data::NOTOSERIF_AUTOHINT_SHAPING).unwrap();
        let shaper = Shaper::new(&font, ShaperMode::Nominal);
        let style = &style::STYLE_CLASSES[style::StyleClass::LATN_C2SC];
        let mut cluster_shaper = shaper.cluster_shaper(style);
        let mut cluster = ShapedCluster::new();
        cluster_shaper.shape("H", &mut cluster);
        assert_eq!(cluster.len(), 1);
        // from ttx, gid 1 is "H"
        assert_eq!(cluster[0].id, GlyphId::new(1));
    }

    #[test]
    fn exceed_max_depth() {
        let font = FontRef::new(font_test_data::NOTOSERIF_AUTOHINT_SHAPING).unwrap();
        let shaper = Shaper::new(&font, ShaperMode::BestEffort);
        let style = &style::STYLE_CLASSES[style::StyleClass::LATN];
        // Build a lookup chain exceeding our max depth
        let mut bad_lookup_builder = BadLookupBuilder::default();
        for i in 0..MAX_NESTING_DEPTH {
            // each lookup calls the next
            bad_lookup_builder.lookups.push(i as u16 + 1);
        }
        let lookup_list_buf = bad_lookup_builder.build();
        let lookup_list = SubstitutionLookupList::read(FontData::new(&lookup_list_buf)).unwrap();
        let mut set_buf = [0u8; 8192];
        let mut visited_set = VisitedLookupSet(&mut set_buf);
        let mut gsub_handler = GsubHandler::new(
            &shaper.charmap,
            &lookup_list,
            style,
            &mut [],
            &mut visited_set,
        );
        assert_eq!(
            gsub_handler.process_lookup(0),
            Err(ProcessLookupError::ExceededMaxDepth)
        );
    }

    #[test]
    fn dont_cycle_forever() {
        let font = FontRef::new(font_test_data::NOTOSERIF_AUTOHINT_SHAPING).unwrap();
        let shaper = Shaper::new(&font, ShaperMode::BestEffort);
        let style = &style::STYLE_CLASSES[style::StyleClass::LATN];
        // Build a lookup chain that cycles; 0 calls 1 which calls 0
        let mut bad_lookup_builder = BadLookupBuilder::default();
        bad_lookup_builder.lookups.push(1);
        bad_lookup_builder.lookups.push(0);
        let lookup_list_buf = bad_lookup_builder.build();
        let lookup_list = SubstitutionLookupList::read(FontData::new(&lookup_list_buf)).unwrap();
        let mut set_buf = [0u8; 8192];
        let mut visited_set = VisitedLookupSet(&mut set_buf);
        let mut gsub_handler = GsubHandler::new(
            &shaper.charmap,
            &lookup_list,
            style,
            &mut [],
            &mut visited_set,
        );
        gsub_handler.process_lookup(0).unwrap();
    }

    #[test]
    fn visited_set() {
        let count = 2341u16;
        let n_bytes = (count as usize).div_ceil(8);
        let mut set_buf = vec![0u8; n_bytes];
        let mut set = VisitedLookupSet::new(&mut set_buf);
        for i in 0..count {
            assert!(set.insert(i));
            assert!(!set.insert(i));
        }
        for byte in &set_buf[0..set_buf.len() - 1] {
            assert_eq!(*byte, 0xFF);
        }
        assert_eq!(*set_buf.last().unwrap(), 0b00011111);
    }

    #[derive(Default)]
    struct BadLookupBuilder {
        /// Just a list of nested lookup indices for each generated lookup
        lookups: Vec<u16>,
    }

    impl BadLookupBuilder {
        fn build(&self) -> Vec<u8> {
            // Full byte size of a contextual format 3 lookup with one
            // subtable and one nested lookup
            const CONTEXT3_FULL_SIZE: usize = 18;
            let mut buf = BeBuffer::default();
            // LookupList table
            // count
            buf = buf.push(self.lookups.len() as u16);
            // offsets for each lookup
            let base_offset = 2 + 2 * self.lookups.len();
            for i in 0..self.lookups.len() {
                buf = buf.push((base_offset + i * CONTEXT3_FULL_SIZE) as u16);
            }
            // now the actual lookups
            for nested_ix in &self.lookups {
                // lookup type: GSUB contextual substitution
                buf = buf.push(5u16);
                // lookup flag
                buf = buf.push(0u16);
                // subtable count
                buf = buf.push(1u16);
                // offset to single subtable (always 8 bytes from start of lookup)
                buf = buf.push(8u16);
                // start of subtable, format == 3
                buf = buf.push(3u16);
                // number of glyphs in sequence
                buf = buf.push(0u16);
                // sequence lookup count
                buf = buf.push(1u16);
                // (no coverage offsets)
                // sequence lookup (sequence index, lookup index)
                buf = buf.push(0u16).push(*nested_ix);
            }
            buf.to_vec()
        }
    }
}
