// Copyright 2022 the Tectonic Project
// Licensed under the MIT License.

//! Assets generated by a Tectonic HTML build.

use serde::Serialize;
use std::{
    borrow::Cow,
    collections::{hash_map::Iter, HashMap},
    fs::File,
    io::{Read, Write},
    path::{Path, PathBuf},
};
use tectonic_errors::{anyhow::Context, prelude::*};
use tectonic_status_base::tt_warning;

use crate::{fonts::FontEnsemble, specials::Special, Common};

/// Runtime state about which non-font assets have been created.
#[derive(Debug, Default)]
pub(crate) struct Assets {
    paths: HashMap<String, AssetOrigin>,
}

/// Different kinds of non-font assets that can be defined at runtime.
#[derive(Debug)]
enum AssetOrigin {
    /// Copy a file from the source stack directly to the output directory.
    Copy(String),

    /// Emit a CSS file containing information about the ensemble of fonts
    /// that have been used.
    FontCss,
}

impl Assets {
    /// Returns true if the special was successfully handled. The false case
    /// doesn't distinguish between a special that wasn't relevant, and one that
    /// was malformatted or otherwise unparseable.
    pub fn try_handle_special(&mut self, special: Special, common: &mut Common) -> bool {
        match special {
            Special::ProvideFile(spec) => {
                let (src_tex_path, dest_path) = match spec.split_once(' ') {
                    Some(t) => t,
                    None => {
                        tt_warning!(common.status, "ignoring malformatted special `{}`", special);
                        return false;
                    }
                };

                self.copy_file(src_tex_path, dest_path);
                true
            }

            Special::ProvideSpecial(spec) => {
                let (kind, dest_path) = match spec.split_once(' ') {
                    Some(t) => t,
                    None => {
                        tt_warning!(common.status, "ignoring malformatted special `{}`", special);
                        return false;
                    }
                };

                match kind {
                    "font-css" => {
                        self.emit_font_css(dest_path);
                        true
                    }
                    _ => {
                        tt_warning!(common.status, "ignoring unsupported special `{}`", special);
                        false
                    }
                }
            }

            _ => false,
        }
    }

    fn copy_file<S1: ToString, S2: ToString>(&mut self, src_path: S1, dest_path: S2) {
        self.paths.insert(
            dest_path.to_string(),
            AssetOrigin::Copy(src_path.to_string()),
        );
    }

    fn emit_font_css<S: ToString>(&mut self, dest_path: S) {
        self.paths
            .insert(dest_path.to_string(), AssetOrigin::FontCss);
    }

    /// This functional must only be called if `common.out_path` is not None.
    pub(crate) fn emit(mut self, mut fonts: FontEnsemble, common: &mut Common) -> Result<()> {
        let faces = fonts.emit(common.out_base)?;

        for (dest_path, origin) in self.paths.drain() {
            match origin {
                AssetOrigin::Copy(ref src_path) => emit_copied_file(src_path, &dest_path, common),
                AssetOrigin::FontCss => emit_font_css(&dest_path, &faces, common),
            }?;
        }

        Ok(())
    }

    pub(crate) fn into_serialize(mut self, fonts: FontEnsemble) -> impl Serialize {
        let (mut assets, css_data) = fonts.into_serialize();

        for (dest_path, origin) in self.paths.drain() {
            let info = match origin {
                AssetOrigin::Copy(src_path) => syntax::AssetOrigin::Copy(src_path),
                AssetOrigin::FontCss => syntax::AssetOrigin::FontCss(css_data.clone()),
            };
            assets.0.insert(dest_path, info);
        }

        assets
    }
}

/// This functional must only be called if `common.out_path` is not None.
fn emit_copied_file(src_tex_path: &str, dest_path: &str, common: &mut Common) -> Result<()> {
    let mut ih = atry!(
        common.hooks.io().input_open_name(src_tex_path, common.status).must_exist();
        ["unable to open provideFile source `{}`", &src_tex_path]
    );

    {
        let (mut out_file, out_path) = create_asset_file(dest_path, common)?;

        atry!(
            std::io::copy(&mut ih, &mut out_file);
            ["cannot copy to output file `{}`", out_path.display()]
        );
    }

    let (name, digest_opt) = ih.into_name_digest();
    common
        .hooks
        .event_input_closed(name, digest_opt, common.status);
    Ok(())
}

/// This functional must only be called if `common.out_path` is not None.
fn emit_font_css(dest_path: &str, faces: &str, common: &mut Common) -> Result<()> {
    let (mut out_file, out_path) = create_asset_file(dest_path, common)?;

    atry!(
        write!(&mut out_file, "{faces}");
        ["cannot write output file `{}`", out_path.display()]
    );

    Ok(())
}

/// This functional must only be called if `common.out_path` is not None.
fn create_asset_file(dest_path: &str, common: &mut Common) -> Result<(File, PathBuf)> {
    let out_path = create_output_path(dest_path, common)?.0.unwrap();

    let out_file = atry!(
        File::create(&out_path);
        ["cannot open output file `{}`", out_path.display()]
    );

    Ok((out_file, out_path))
}

/// Process a TeX output path into one for the actual filesystem.
///
/// We have a separate argument `do_create`, rather than just looking at
/// `common.do_not_emit`, since this function is used for assets as well as
/// templated HTML outputs.
pub(crate) fn create_output_path(
    dest_path: &str,
    common: &mut Common,
) -> Result<(Option<PathBuf>, usize)> {
    let mut out_path = common.out_base.map(|p| p.to_owned());
    let mut n_levels = 0;

    for piece in dest_path.split('/') {
        if let Some(out_path) = out_path.as_mut() {
            match std::fs::create_dir(out_path.as_path()) {
                Ok(_) => {}
                Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
                Err(e) => {
                    return Err(e).context(format!(
                        "cannot create output parent directory `{}`",
                        out_path.display()
                    ));
                }
            }
        }

        if piece.is_empty() {
            continue;
        }

        if piece == ".." {
            bail!(
                "illegal provideFile dest path `{}`: it contains a `..` component",
                dest_path
            );
        }

        let as_path = Path::new(piece);

        if as_path.is_absolute() || as_path.has_root() {
            bail!(
                "illegal provideFile path `{}`: it contains an absolute/rooted component",
                dest_path,
            );
        }

        if let Some(out_path) = out_path.as_mut() {
            out_path.push(piece);
        }

        n_levels += 1;
    }

    Ok((out_path, n_levels))
}

/// Information about assets that have been defined in an SPX-to-HTML run.
#[derive(Clone, Debug, Default)]
pub struct AssetSpecification(syntax::Assets);

impl AssetSpecification {
    /// Update this specification with information from one that's been
    /// serialized.
    ///
    /// It is possible for two specifications to be incompatible, in which case
    /// an error will be returned and this object will be left in an undefined
    /// state.
    pub fn add_from_saved<R: Read>(&mut self, reader: R) -> Result<&mut Self> {
        let new: syntax::Assets = atry!(
            serde_json::from_reader(reader);
            ["failed to deserialize saved specification"]
        );

        // As things are currently structured, we can parse the new entries in
        // any order. This is because we assume that the both inputs (self and
        // the new one) have internally-consistent cross-referencing, in which
        // case their merger must as well. (Here, "cross-referencing" means
        // aspects like the font-family information referencing output filenames
        // for font-files.)

        use syntax::AssetOrigin as AO;

        for (path, new_origin) in &new.0 {
            if let Some(cur_origin) = self.0 .0.get_mut(path) {
                match (new_origin, cur_origin) {
                    (AO::Copy(new_src), AO::Copy(cur_src)) => {
                        if cur_src != new_src {
                            bail!(
                                "disagreeing sources `{}` and `{}` for copied output asset `{}`",
                                cur_src,
                                new_src,
                                path
                            );
                        }
                    }

                    (AO::FontFile(new_ff), AO::FontFile(cur_ff)) => {
                        if new_ff.source != cur_ff.source {
                            bail!(
                                "disagreeing sources `{}` and `{}` for output font asset `{}`",
                                cur_ff.source,
                                new_ff.source,
                                path
                            );
                        }

                        if new_ff.face_index != cur_ff.face_index {
                            bail!(
                                "disagreeing face indices `{}` and `{}` for output font asset `{}`",
                                cur_ff.face_index,
                                new_ff.face_index,
                                path
                            );
                        }

                        // We have two font assets with the same source. We need
                        // to merge the vglyph information, but otherwise we're
                        // good!
                        syntax::merge_vglyphs(&mut cur_ff.vglyphs, &new_ff.vglyphs);
                    }

                    (AO::FontCss(new_fe), AO::FontCss(cur_fe)) => {
                        // We have two font ensembles. Try merging.
                        syntax::merge_font_ensembles(&mut cur_fe.0, &new_fe.0)?;
                    }

                    (new2, cur2) => {
                        bail!(
                            "disagreeing origin types {} and {} for output asset `{}`",
                            cur2,
                            new2,
                            path
                        );
                    }
                }
            } else {
                // This path is undefined in the current object. Just add it!
                self.0 .0.insert(path.clone(), new_origin.clone());
            }
        }

        Ok(self)
    }

    /// Save this asset specification to a stream.
    ///
    /// Currently, this is done in a JSON format, but this is not guaranteed to
    /// always be the case. The serialization format does not make any effort to
    /// provide for backwards or forwards compatibility. The serialized data
    /// should be viewed as ephemera that are only guaranteed to remain useful
    /// so long as the executing program remains unchanged.
    pub fn save<W: Write>(&self, writer: W) -> Result<()> {
        serde_json::to_writer_pretty(writer, &self.0).map_err(|e| e.into())
    }

    /// Produce the TeX paths of the output files associated with this
    /// specification.
    pub fn output_paths(&self) -> impl Iterator<Item = Cow<'_, str>> {
        AssetOutputsIterator {
            iter: self.0 .0.iter(),
            cur_vg_path: None,
            next_vg_index: 0,
        }
    }

    /// Check that a set of fonts defined at runtime are a subset of those
    /// defined in this specification.
    ///
    /// This function is used in the "precomputed assets" mode, to make sure
    /// that the SPX file doesn't set up any font configuration that we didn't
    /// expect.
    pub(crate) fn check_runtime_fonts(
        &self,
        fonts: &mut FontEnsemble,
        common: &mut Common,
    ) -> Result<()> {
        fonts.match_to_precomputed(&self.0, common)
    }

    /// Check that the assets defined at runtime are a subset of those defined
    /// in this specification, and update them to cover the specification.
    ///
    /// This function is used in the "precomputed assets" mode, to make sure
    /// that the SPX file doesn't try to define anything that we didn't expect.
    /// Fonts have already been looked at, so we just need to check output
    /// filenames. We also need to update the collection of runtime assets so
    /// that if we are asked to emit assets, we'll emit *everything*, not just
    /// the ones this particular session knows about.
    pub(crate) fn check_runtime_assets(&self, assets: &mut Assets) -> Result<()> {
        for (path, run_origin) in &assets.paths {
            if let Some(pre_origin) = self.0 .0.get(path) {
                match (run_origin, pre_origin) {
                    (AssetOrigin::Copy(run_path), syntax::AssetOrigin::Copy(pre_path)) => {
                        ensure!(
                            run_path == pre_path,
                            "asset `{}` should \
                            copy out path `{}`, but in this session the source is `{}`",
                            path,
                            pre_path,
                            run_path
                        );
                    }

                    (AssetOrigin::FontCss, syntax::AssetOrigin::FontCss(_)) => {}

                    _ => {
                        bail!(
                            "this session and the precomputed assets disagree on `{}`",
                            path
                        );
                    }
                }
            } else {
                bail!(
                    "this session defines an asset at `{}` that is not in the precomputed bundle",
                    path
                );
            }
        }

        // Now update the runtime assets to include all precomputed ones.

        for (path, pre_origin) in &self.0 .0 {
            let mapped = match pre_origin {
                syntax::AssetOrigin::Copy(pre_path) => AssetOrigin::Copy(pre_path.to_owned()),
                syntax::AssetOrigin::FontCss(_) => AssetOrigin::FontCss,
                syntax::AssetOrigin::FontFile(_) => continue,
            };

            assets.paths.entry(path.to_owned()).or_insert(mapped);
        }

        Ok(())
    }
}

struct AssetOutputsIterator<'a> {
    iter: Iter<'a, String, syntax::AssetOrigin>,
    cur_vg_path: Option<String>,
    next_vg_index: usize,
}

impl<'a> Iterator for AssetOutputsIterator<'a> {
    type Item = Cow<'a, str>;

    fn next(&mut self) -> Option<Cow<'a, str>> {
        if let Some(p) = self.cur_vg_path.as_ref() {
            let rv = Cow::Owned(format!("vg{}{}", self.next_vg_index, p));

            if self.next_vg_index == 0 {
                self.cur_vg_path = None;
            } else {
                self.next_vg_index -= 1;
            }

            return Some(rv);
        }

        self.iter.next().map(|(path, origin)| {
            if let syntax::AssetOrigin::FontFile(ref ffi) = origin {
                if !ffi.vglyphs.is_empty() {
                    // If we have moved on to a font file with variant glyphs,
                    // we first (now) yield the unmodified filename, then set up
                    // to iterate through the `vg` versions.
                    let mut highest_vg_index = 0;

                    for mapping in ffi.vglyphs.values() {
                        highest_vg_index = std::cmp::max(highest_vg_index, mapping.index);
                    }

                    self.cur_vg_path = Some(path.to_owned());
                    self.next_vg_index = highest_vg_index;
                }
            }

            Cow::Borrowed(path.as_ref())
        })
    }
}

/// The concrete syntax for saving asset-output state, wired up via serde.
///
/// The top-level type is Assets.
pub(crate) mod syntax {
    use serde::{Deserialize, Serialize, Serializer};
    use std::collections::{BTreeMap, HashMap};
    use tectonic_errors::prelude::*;

    /// Annoyingly we need to wrap this hashmap in a struct because we need to
    /// customize the serializer to sort the keys for reproducible outputs.
    /// Likewise for all other hashmaps in this module.
    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
    pub struct Assets(#[serde(serialize_with = "ordered_map")] pub HashMap<String, AssetOrigin>);

    fn ordered_map<K: Ord + Serialize, V: Serialize, S>(
        value: &HashMap<K, V>,
        serializer: S,
    ) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let ordered: BTreeMap<_, _> = value.iter().collect();
        ordered.serialize(serializer)
    }

    #[derive(Clone, Debug, Deserialize, Serialize)]
    #[serde(tag = "kind")]
    pub enum AssetOrigin {
        /// Copy a file from the source stack directly to the output directory.
        Copy(String),

        /// Emit a CSS file containing information about the ensemble of fonts
        /// that have been used.
        FontCss(FontEnsembleAssetData),

        /// An OpenType/TrueType font file and variants with customized CMAP tables
        /// allowing access to unusual glyphs.
        FontFile(FontFileAssetData),
    }

    impl std::fmt::Display for AssetOrigin {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
            match self {
                AssetOrigin::Copy(src) => write!(f, "copy out `{src}`"),

                AssetOrigin::FontCss(fe) => {
                    let mut first = true;

                    write!(f, "CSS for font faces")?;

                    for facename in fe.0.keys() {
                        if first {
                            write!(f, " ")?;
                            first = false;
                        } else {
                            write!(f, ", ")?;
                        }

                        write!(f, "\"{facename}\"")?;
                    }

                    Ok(())
                }

                AssetOrigin::FontFile(ff) => {
                    write!(f, "font face #{} from `{}`", ff.face_index, ff.source)
                }
            }
        }
    }

    #[derive(Clone, Debug, Default, Deserialize, Serialize)]
    pub struct FontFileAssetData {
        /// The path to find the font file in the source stack.
        pub source: String,

        /// The face index of this font in the source file.
        pub face_index: u32,

        /// Variant glyphs that require us to emit variant versions of the font
        /// file.
        ///
        /// Due to limitations of (serde's) JSON serialization, the keys of this
        /// dictionary have to be strings, even though we would like them to be
        /// GlyphIds.
        #[serde(serialize_with = "ordered_map")]
        pub vglyphs: HashMap<String, GlyphVariantMapping>,
    }

    /// Merge one table of variant glyph USV mappings into another.
    pub(crate) fn merge_vglyphs(
        cur: &mut HashMap<String, GlyphVariantMapping>,
        new: &HashMap<String, GlyphVariantMapping>,
    ) {
        // First, get the maximum seen index for each USV.

        let mut next_index = HashMap::new();

        for mapping in cur.values() {
            let idx = next_index.entry(mapping.usv).or_default();
            *idx = std::cmp::max(*idx, mapping.index + 1);
        }

        // Now add mappings for any new glyphs that we need.

        for (gid, mapping) in new {
            // If the glyph is already in the "cur" mapping, great. If not, add
            // a new mapping, using the "new" map's suggested USV.
            cur.entry(gid.clone()).or_insert_with(|| {
                let next_idx = next_index.entry(mapping.usv).or_default();
                let index = *next_idx;
                *next_idx = index + 1;
                GlyphVariantMapping {
                    usv: mapping.usv,
                    index,
                }
            });
        }
    }

    #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
    pub struct GlyphVariantMapping {
        /// The USV that the glyph should be mapped to
        pub usv: char,

        /// Which alternative-mapped font to use. These indices start at zero.
        pub index: usize,
    }

    impl From<crate::fontfile::GlyphVariantMapping> for GlyphVariantMapping {
        fn from(m: crate::fontfile::GlyphVariantMapping) -> Self {
            GlyphVariantMapping {
                usv: m.usv,
                index: m.variant_map_index,
            }
        }
    }

    /// Map from symbolic family name to info about the fonts defining it.
    #[derive(Clone, Debug, Default, Deserialize, Serialize)]
    pub struct FontEnsembleAssetData(
        #[serde(serialize_with = "ordered_map")] pub HashMap<String, FontFamilyAssetData>,
    );

    /// Merge one font ensemble (table of font-family definitions) into another.
    /// This can fail if the tables are not self-consistent.
    pub fn merge_font_ensembles(
        cur: &mut HashMap<String, FontFamilyAssetData>,
        new: &HashMap<String, FontFamilyAssetData>,
    ) -> Result<()> {
        for (name, new_ff) in new {
            if let Some(cur_ff) = cur.get_mut(name) {
                for (facetype, new_facepath) in &new_ff.faces {
                    if let Some(cur_facepath) = cur_ff.faces.get(facetype) {
                        // This facetype is already defined in this family --
                        // check that we agree on what font it is.
                        if cur_facepath != new_facepath {
                            bail!(
                                "disagreeing asset paths for font family {}/{:?}: `{}` and `{}`",
                                name,
                                facetype,
                                cur_facepath,
                                new_facepath
                            );
                        }
                    } else {
                        // This facetype is new for this family.
                        cur_ff.faces.insert(*facetype, new_facepath.clone());
                    }
                }
            } else {
                // This family is a new definition. Just copy it.
                cur.insert(name.clone(), new_ff.clone());
            }
        }

        Ok(())
    }

    #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
    pub struct FontFamilyAssetData {
        /// Map from face type to the output path of the font file providing it.
        #[serde(serialize_with = "ordered_map")]
        pub faces: HashMap<FaceType, String>,
    }

    #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
    pub enum FaceType {
        /// The regular (upright) font of a font family.
        Regular,

        /// The bold font of a family.
        Bold,

        /// The italic font of a family.
        Italic,

        /// The bold-italic font a current family.
        BoldItalic,
    }
}
