//! The result of a compilation

use std::collections::HashMap;

use write_fonts::{
    BuilderError, FontBuilder,
    tables::{
        self as wtables, gdef::GlyphClassDef, layout::FeatureParams, maxp::Maxp, stat::AxisValue,
    },
    types::{GlyphId16, NameId},
};

use super::Opts;

use crate::GlyphMap;

/// The tables generated by this compilation.
///
/// All tables are optional, and the set of tables that are present depends
/// on the input file.
///
/// Each table is a type defined in the [`write-fonts`][] crate. The caller
/// may either interact with these directly, or else they may use the [`to_binary`]
/// method to generate a binary font.
///
/// [`to_binary`]: Compilation::to_binary
/// [`write-fonts`]: https://docs.rs/write-fonts/latest/write_fonts/
pub struct Compilation {
    /// The options passed in for this compilation.
    pub(crate) opts: Opts,
    /// The `head` table, if one was generated
    pub head: Option<wtables::head::Head>,
    /// The `hhea` table, if one was generated
    pub hhea: Option<wtables::hhea::Hhea>,
    /// The `vhea` table, if one was generated
    pub vhea: Option<wtables::vhea::Vhea>,
    /// The `OS/2` table, if one was generated
    pub os2: Option<wtables::os2::Os2>,
    /// The `GDEF` table, if one was generated
    pub gdef: Option<wtables::gdef::Gdef>,
    /// The `BASE` table, if one was generated
    pub base: Option<wtables::base::Base>,
    /// The `name` table, if one was generated
    pub name: Option<wtables::name::Name>,
    /// The `STAT` table, if one was generated
    pub stat: Option<wtables::stat::Stat>,
    /// The `GSUB` table, if one was generated
    pub gsub: Option<wtables::gsub::Gsub>,
    /// The `GPOS` table, if one was generated
    pub gpos: Option<wtables::gpos::Gpos>,
    /// Any *explicit* gdef classes declared in the FEA.
    ///
    /// This is provided so that the user can reference them if they are going
    /// to manually generate kerning or markpos lookups.
    pub gdef_classes: Option<HashMap<GlyphId16, GlyphClassDef>>,
}

impl Compilation {
    /// Returns `true` if the FEA generated tables other than GSUB, GPOS & GDEF.
    pub fn has_non_layout_tables(&self) -> bool {
        self.head.is_some()
            || self.hhea.is_some()
            || self.vhea.is_some()
            || self.os2.is_some()
            || self.base.is_some()
            || self.name.is_some()
            || self.stat.is_some()
    }

    /// Remap any `NameId`s in the name table and anywhere they are referenced.
    ///
    /// This is used for merging the results of our compilation with other
    /// compilation operations which may have occured elsewhere, and which may
    /// have used the same `NameId`s as us for different strings.
    ///
    /// We will take all the name ids we have declared that are >= 256 and offset
    /// them to start at `first_avail_id`.
    pub fn remap_name_ids(&mut self, first_avail_id: u16) {
        let id_offset = first_avail_id.saturating_sub(NameId::LAST_RESERVED_NAME_ID.to_u16() + 1);
        log::info!("remapping FEA name ideas with delta {id_offset}");
        if id_offset == 0 {
            return;
        }

        let adjust_id = |id: NameId| {
            if !id.is_reserved() {
                // in the case of a degenerate name table we'll just reuse the last id?
                // entries will be pruned later.
                id.checked_add(id_offset)
                    .unwrap_or(NameId::LAST_ALLOWED_NAME_ID)
            } else {
                id
            }
        };

        if let Some(name) = self.name.as_mut() {
            let records = std::mem::take(&mut name.name_record);
            name.name_record = records
                .into_iter()
                .map(|mut rec| {
                    rec.name_id = adjust_id(rec.name_id);
                    rec
                })
                .collect();
        }

        if let Some(gsub) = self.gsub.as_mut() {
            gsub.feature_list
                .as_mut()
                .feature_records
                .iter_mut()
                .for_each(|rec| match rec.feature.as_mut().feature_params.as_mut() {
                    Some(FeatureParams::StylisticSet(params)) => {
                        params.ui_name_id = adjust_id(params.ui_name_id);
                    }
                    Some(FeatureParams::CharacterVariant(params)) => {
                        params.feat_ui_label_name_id = adjust_id(params.feat_ui_label_name_id);
                        params.feat_ui_tooltip_text_name_id =
                            adjust_id(params.feat_ui_tooltip_text_name_id);
                        params.sample_text_name_id = adjust_id(params.sample_text_name_id);
                        params.first_param_ui_label_name_id =
                            adjust_id(params.first_param_ui_label_name_id);
                    }
                    _ => (),
                });
        }
        if let Some(stat) = self.stat.as_mut() {
            stat.elided_fallback_name_id = stat
                .elided_fallback_name_id
                .map(|id| id.to_u16().saturating_add(id_offset).into());
            stat.design_axes.iter_mut().for_each(|axe| {
                axe.axis_name_id = adjust_id(axe.axis_name_id);
            });
            if let Some(blah) = stat.offset_to_axis_values.as_mut() {
                blah.iter_mut().for_each(|val| match val.as_mut() {
                    AxisValue::Format1(val) => val.value_name_id = adjust_id(val.value_name_id),
                    AxisValue::Format2(val) => val.value_name_id = adjust_id(val.value_name_id),
                    AxisValue::Format3(val) => val.value_name_id = adjust_id(val.value_name_id),
                    AxisValue::Format4(val) => val.value_name_id = adjust_id(val.value_name_id),
                });
            }
        }
    }

    /// Assemble the output tables into a `FontBuilder`.
    ///
    /// This is a convenience method. To compile a binary font you can use
    /// [`to_binary`] instead, and for more fine-grained control you can inspect
    /// and manipulate the raw tables directly.
    ///
    /// [`to_binary`]: Compilation::to_binary
    pub fn to_font_builder(&self) -> Result<FontBuilder<'_>, BuilderError> {
        let mut builder = FontBuilder::default();
        macro_rules! add_if_some {
            ($table:expr_2021) => {
                if let Some(table) = $table.as_ref() {
                    builder.add_table(table)?;
                }
            };
        }
        add_if_some!(self.head);
        add_if_some!(self.hhea);
        add_if_some!(self.vhea);
        add_if_some!(self.os2);
        add_if_some!(self.gdef);
        add_if_some!(self.base);
        add_if_some!(self.name);
        add_if_some!(self.stat);
        add_if_some!(self.gsub);
        add_if_some!(self.gpos);
        Ok(builder)
    }

    /// Compile the output tables into a font.
    ///
    /// This is a convenience method used for things like testing; if you are
    /// building a font compiler you will probably prefer to manipulate the
    /// generated tables directly.
    pub fn to_binary(&self, glyph_map: &GlyphMap) -> Result<Vec<u8>, BuilderError> {
        // because we often inspect our output with ttx, and ttx fails if maxp is
        // missing, we create a maxp table.
        let mut builder = self.to_font_builder()?;
        let maxp = Maxp::new(glyph_map.len().try_into().unwrap());
        builder.add_table(&maxp)?;
        if self.opts.make_post_table {
            let post = glyph_map.make_post_table();
            builder.add_table(&post)?;
        }

        Ok(builder.build())
    }
}
