// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    super::{
        inspect::zero_pad,
        typeface::{
            Collection as TypefaceCollection, Typeface, TypefaceCollectionBuilder, TypefaceId,
            TypefaceInfoAndCharSet, TypefaceInspectData,
        },
        AssetId,
    },
    fidl_fuchsia_fonts::GenericFontFamily,
    fuchsia_inspect as finspect,
    heck::KebabCase,
    itertools::Itertools,
    manifest::{serde_ext::StyleOptions, v2},
    std::{collections::BTreeMap, sync::Arc},
    unicase::UniCase,
};

#[derive(Debug, Eq, Hash, PartialEq)]
pub enum FamilyOrAlias {
    Family(FontFamily),
    /// The definition of a `Family`'s alias. This is a representation of the alias's _target_
    /// (value). The alias's name (key) is stored outside of this enum.
    ///
    /// * `UniCase<String>` - The _canonical_ family name.
    /// * `Option<Arc<TypefaceQueryOverrides>>` - Optional query overrides applied when a client
    ///    requests the associated alias.
    Alias(UniCase<String>, Option<Arc<TypefaceQueryOverrides>>),
}

/// Builder for [FamilyOrAlias].
#[derive(Debug)]
pub enum FamilyOrAliasBuilder {
    Family(FontFamilyBuilder),
    Alias(UniCase<String>, Option<Arc<TypefaceQueryOverrides>>),
}

impl FamilyOrAliasBuilder {
    /// Finalizes and builds a `FamilyOrAlias`.
    pub fn build(self) -> FamilyOrAlias {
        match self {
            FamilyOrAliasBuilder::Family(family_builder) => {
                FamilyOrAlias::Family(family_builder.build())
            }
            FamilyOrAliasBuilder::Alias(name, overrides) => FamilyOrAlias::Alias(name, overrides),
        }
    }

    /// Generates a full set of aliases from a given manifest v2 `Family`, including alias variants
    /// without spaces.
    pub fn aliases_from_family(
        manifest_family: &v2::Family,
    ) -> BTreeMap<UniCase<String>, FamilyOrAliasBuilder> {
        let mut output: BTreeMap<UniCase<String>, FamilyOrAliasBuilder> = BTreeMap::new();
        let family_name = UniCase::new(manifest_family.name.clone());
        let family_name_without_spaces = UniCase::new(manifest_family.name.replace(" ", ""));
        if family_name_without_spaces != family_name {
            output.insert(
                family_name_without_spaces,
                FamilyOrAliasBuilder::Alias(family_name.clone(), None),
            );
        }

        for alias_set in &manifest_family.aliases {
            let overrides = if alias_set.has_overrides() {
                Some(Arc::new(TypefaceQueryOverrides {
                    style: alias_set.style_overrides().clone(),
                    languages: alias_set.language_overrides().cloned().collect_vec(),
                }))
            } else {
                None
            };

            for alias_name in alias_set.names() {
                let alias_name = UniCase::new(alias_name.clone());
                output.insert(
                    alias_name.clone(),
                    FamilyOrAliasBuilder::Alias(family_name.clone(), overrides.clone()),
                );

                let alias_name_without_spaces = UniCase::new((*alias_name).replace(" ", ""));
                if alias_name != alias_name_without_spaces {
                    output.insert(
                        alias_name_without_spaces,
                        FamilyOrAliasBuilder::Alias(family_name.clone(), overrides.clone()),
                    );
                }
            }
        }

        output
    }
}

/// Optional set of typeface query overrides embedded in a `FontFamilyAlias`.
///
/// For example, the if a client asks for alias "Roboto Condensed", the server knows that means
/// means the canonical family "Roboto" with style `width: condensed`. If the client asks for "Noto
/// Sans CJK JP", that's interpreted as the canonical family "Noto Sans CJK" with
/// `languages: [ "ja" ]`.
///
#[derive(Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub struct TypefaceQueryOverrides {
    pub style: StyleOptions,
    pub languages: Vec<String>,
}

impl TypefaceQueryOverrides {
    pub fn has_style_overrides(&self) -> bool {
        self.style != StyleOptions::default()
    }

    pub fn has_language_overrides(&self) -> bool {
        !self.languages.is_empty()
    }
}

/// In-memory representation of metadata for a font family and its typefaces.
#[derive(Debug, Eq, PartialEq, Hash)]
pub struct FontFamily {
    /// The family's canonical name
    pub name: String,
    /// Collection of typefaces in the family
    pub faces: TypefaceCollection,
    /// Generic font family that this family belongs to, if applicable
    pub generic_family: Option<GenericFontFamily>,
}

impl FontFamily {
    /// Get owned copies of the family's typefaces as `TypefaceInfo`
    pub fn extract_faces<'a>(&'a self) -> impl Iterator<Item = TypefaceInfoAndCharSet> + 'a {
        // Convert Vec<Arc<Typeface>> to Vec<TypefaceInfo>
        self.faces
            .faces
            .iter()
            // Copy most fields from `Typeface` and use the canonical family name
            .map(move |face| TypefaceInfoAndCharSet::from_typeface(face, self.name.clone()))
    }
}

/// Builder for [FontFamily].
#[derive(Debug)]
pub struct FontFamilyBuilder {
    /// The family's canonical name
    name: String,
    /// Generic font family that this family belongs to, if applicable
    generic_family: Option<GenericFontFamily>,
    /// Collection of typefaces in the family
    faces: TypefaceCollectionBuilder,
}

impl FontFamilyBuilder {
    /// Creates a new builder without any typefaces defined.
    pub fn new(
        name: impl Into<String>,
        generic_family: Option<GenericFontFamily>,
    ) -> FontFamilyBuilder {
        FontFamilyBuilder {
            name: name.into(),
            generic_family,
            faces: TypefaceCollectionBuilder::new(),
        }
    }

    /// Returns true if the family's typeface collection already has a typeface with the given ID.
    pub fn has_typeface_id(&self, typeface_id: &TypefaceId) -> bool {
        self.faces.has_typeface_id(typeface_id)
    }

    /// If there's no existing typeface with the same typeface ID already in the builder, adds the
    /// given typeface and and returns `true`. Otherwise, returns `false`.
    pub fn add_typeface_once(&mut self, typeface: Arc<Typeface>) -> bool {
        self.faces.add_typeface_once(typeface)
    }

    /// Finalizes and builds a `FontFamily`.
    pub fn build(self) -> FontFamily {
        FontFamily {
            name: self.name,
            faces: self.faces.build(),
            generic_family: self.generic_family,
        }
    }
}

/// Inspect data for a `FontFamily`.
#[derive(Debug)]
pub struct FamilyInspectData {
    node: finspect::Node,

    aliases_node: finspect::Node,
    aliases: Vec<AliasInspectData>,

    typefaces_node: finspect::Node,
    typefaces: Vec<TypefaceInspectData>,
}

impl FamilyInspectData {
    /// Creates a new `FamilyInspectData`.
    pub fn new(
        parent: &finspect::Node,
        family: &FontFamily,
        asset_location_lookup: &impl Fn(AssetId) -> Option<String>,
    ) -> Self {
        let node = parent.create_child(&family.name);

        let aliases_node = node.create_child("aliases");
        let aliases: Vec<AliasInspectData> = Vec::new();

        let typefaces_node = node.create_child("typefaces");
        let typefaces_count = family.faces.faces.len();
        let typefaces: Vec<TypefaceInspectData> = family
            .faces
            .faces
            .iter()
            .enumerate()
            .map(|(idx, typeface)| {
                TypefaceInspectData::new(
                    &typefaces_node,
                    &zero_pad(idx, typefaces_count),
                    typeface,
                    asset_location_lookup,
                )
            })
            .collect();

        FamilyInspectData { node, aliases_node, aliases, typefaces_node, typefaces }
    }

    /// Adds a new font family alias, optionally with query overrides, to the existing Inspect data.
    pub fn add_alias(&mut self, alias: &str, overrides: &Option<Arc<TypefaceQueryOverrides>>) {
        let alias_data = AliasInspectData::new(&self.aliases_node, alias, overrides);
        self.aliases.push(alias_data);
    }
}

/// Inspect data for a single font family alias.
#[derive(Debug)]
pub struct AliasInspectData {
    node: finspect::Node,
    style_overrides: Option<finspect::Node>,
    language_overrides: Option<finspect::StringProperty>,
}

impl AliasInspectData {
    /// Creates a new `AliasInspectData`.
    fn new(
        parent_inspect_node: &finspect::Node,
        alias: &str,
        overrides: &Option<Arc<TypefaceQueryOverrides>>,
    ) -> Self {
        let node = parent_inspect_node.create_child(alias);

        let style_overrides = overrides.as_ref().and_then(|overrides| {
            if overrides.has_style_overrides() {
                let style = node.create_child("style_overrides");
                if let Some(slant) = overrides.style.slant {
                    style.record_string("slant", format!("{:?}", slant).to_kebab_case());
                }
                if let Some(weight) = overrides.style.weight {
                    style.record_uint("weight", weight.into());
                }
                if let Some(width) = overrides.style.width {
                    style.record_string("width", format!("{:?}", width).to_kebab_case());
                }
                Some(style)
            } else {
                None
            }
        });

        let language_overrides = overrides.as_ref().and_then(|overrides| {
            if overrides.has_language_overrides() {
                Some(
                    node.create_string("language_overrides", overrides.languages.iter().join(", ")),
                )
            } else {
                None
            }
        });

        AliasInspectData { node, style_overrides, language_overrides }
    }
}

#[cfg(test)]
mod tests {
    use {
        super::*,
        anyhow::Error,
        char_collection::char_collect,
        fidl_fuchsia_fonts::{Slant, Width},
        finspect::assert_data_tree,
        maplit::{btreemap, btreeset},
        pretty_assertions::assert_eq,
    };

    #[test]
    fn test_aliases_from_family() -> Result<(), Error> {
        let family = v2::Family {
            name: "Family A".to_string(),
            aliases: vec![
                v2::FontFamilyAliasSet::without_overrides(vec!["A Family", "Family Ay"])?,
                v2::FontFamilyAliasSet::new(
                    vec!["Family A Condensed", "Family A Squished"],
                    StyleOptions { width: Some(Width::Condensed), ..Default::default() },
                    std::iter::empty::<String>(),
                )?,
            ],
            generic_family: None,
            assets: vec![],
        };

        let expected = btreemap! {
            UniCase::new("FamilyA".to_string()) =>
                FamilyOrAlias::Alias(UniCase::new("Family A".to_string()), None),
            UniCase::new("A Family".to_string()) =>
                FamilyOrAlias::Alias(UniCase::new("Family A".to_string()), None),
            UniCase::new("AFamily".to_string()) =>
                FamilyOrAlias::Alias(UniCase::new("Family A".to_string()), None),
            UniCase::new("Family A Condensed".to_string()) =>
                FamilyOrAlias::Alias(
                    UniCase::new("Family A".to_string()),
                    Some(Arc::new(
                            TypefaceQueryOverrides {
                                style: StyleOptions {
                                    width: Some(Width::Condensed),
                                    ..Default::default()
                                },
                                languages: vec![]
                            }
                        ))
                ),
            UniCase::new("FamilyACondensed".to_string()) =>
                FamilyOrAlias::Alias(
                    UniCase::new("Family A".to_string()),
                    Some(Arc::new(
                            TypefaceQueryOverrides {
                                style: StyleOptions {
                                    width: Some(Width::Condensed),
                                    ..Default::default()
                                },
                                languages: vec![]
                            }
                        ))
                ),
            UniCase::new("Family A Squished".to_string()) =>
                FamilyOrAlias::Alias(
                    UniCase::new("Family A".to_string()),
                    Some(Arc::new(
                            TypefaceQueryOverrides {
                                style: StyleOptions {
                                    width: Some(Width::Condensed),
                                    ..Default::default()
                                },
                                languages: vec![]
                            }
                        ))
                ),
            UniCase::new("FamilyASquished".to_string()) =>
                FamilyOrAlias::Alias(
                    UniCase::new("Family A".to_string()),
                    Some(Arc::new(
                            TypefaceQueryOverrides {
                                style: StyleOptions {
                                    width: Some(Width::Condensed),
                                    ..Default::default()
                                },
                                languages: vec![]
                            }
                        ))
                ),
            UniCase::new("Family Ay".to_string()) =>
                FamilyOrAlias::Alias(UniCase::new("Family A".to_string()), None),
            UniCase::new("FamilyAy".to_string()) =>
                FamilyOrAlias::Alias(UniCase::new("Family A".to_string()), None),
        };

        let actual: BTreeMap<UniCase<String>, FamilyOrAlias> =
            FamilyOrAliasBuilder::aliases_from_family(&family)
                .into_iter()
                .map(|(key, val)| (key, val.build()))
                .collect();

        assert_eq!(expected, actual);

        Ok(())
    }

    #[test]
    fn test_alias_inspect_data() {
        let alias = "Alif";
        let overrides = Some(Arc::new(TypefaceQueryOverrides {
            style: StyleOptions {
                slant: None,
                weight: Some(600),
                width: Some(Width::ExtraExpanded),
            },
            languages: vec!["ar-EG".to_string(), "ar-MA".to_string()],
        }));

        let inspector = finspect::Inspector::new();
        let _inspect_data = AliasInspectData::new(inspector.root(), alias, &overrides);
        assert_data_tree!(inspector, root: {
            Alif: {
                style_overrides: {
                    weight: 600u64,
                    width: "extra-expanded"
                },
                language_overrides: "ar-EG, ar-MA"
            }
        });
    }

    #[test]
    fn test_family_inspect_data() {
        let mut family = FontFamilyBuilder::new("Alpha", Some(GenericFontFamily::Cursive));
        family.add_typeface_once(Arc::new(Typeface {
            asset_id: AssetId(0),
            font_index: 0,
            slant: Slant::Upright,
            weight: 400,
            width: Width::Normal,
            languages: btreeset!("ar-EG".to_string(), "ar-MA".to_string()),
            char_set: char_collect!(0x0..=0xEE).into(),
            generic_family: Some(GenericFontFamily::Cursive),
        }));
        family.add_typeface_once(Arc::new(Typeface {
            asset_id: AssetId(1),
            font_index: 0,
            slant: Slant::Upright,
            weight: 400,
            width: Width::UltraCondensed,
            languages: btreeset!("und-Latn".to_string()),
            char_set: char_collect!(0x0..=0xEE).into(),
            generic_family: Some(GenericFontFamily::Cursive),
        }));
        let family = family.build();

        let asset_location_lookup = |asset_id| match asset_id {
            AssetId(0) => Some("/path/to/asset-0.ttf".to_string()),
            AssetId(1) => Some("/path/to/asset-1.ttf".to_string()),
            _ => unreachable!(),
        };

        let inspector = finspect::Inspector::new();
        let mut inspect_data =
            FamilyInspectData::new(inspector.root(), &family, &asset_location_lookup);
        inspect_data.add_alias(
            "Alif",
            &Some(Arc::new(TypefaceQueryOverrides {
                style: StyleOptions::default(),
                languages: vec!["ar-EG".to_string(), "ar-JO".to_string()],
            })),
        );
        inspect_data.add_alias("Ay", &None);

        // The contents of `AliasInspectData` and `TypefaceInspectData` are tested in their
        // respective unit tests. This test just asserts the overall structure.
        assert_data_tree!(inspector, root: {
            Alpha: {
                aliases: {
                    Ay: contains {},
                    Alif: contains {},
                },
                typefaces: {
                    "0": contains {
                        asset_location: "/path/to/asset-0.ttf",
                    },
                    "1": contains {
                        asset_location: "/path/to/asset-1.ttf",
                    },
                }
            }
        });
    }
}
