use std::{
    collections::HashSet,
    fmt::{Display, Formatter},
    num::NonZeroUsize,
    rc::Rc,
};

use log::trace;
use lru::LruCache;
use skia_safe::{Data, Font, FontHinting as SkiaHinting, FontMgr, font::Edging as SkiaEdging};

use crate::{
    profiling::tracy_zone,
    renderer::fonts::{
        font_options::{CoarseStyle, FontDescription, FontEdging, FontHinting},
        swash_font::SwashFont,
    },
};

static DEFAULT_FONT: &[u8] = include_bytes!("../../../assets/fonts/FiraCodeNerdFont-Regular.ttf");
static LAST_RESORT_FONT: &[u8] = include_bytes!("../../../assets/fonts/LastResort-Regular.ttf");

pub struct FontPair {
    pub key: FontKey,
    pub skia_font: Font,
    pub swash_font: SwashFont,
}

impl FontPair {
    fn new(key: FontKey, mut skia_font: Font) -> Option<FontPair> {
        skia_font.set_subpixel(true);
        skia_font.set_baseline_snap(true);
        skia_font.set_hinting(font_hinting(&key.hinting));
        skia_font.set_edging(font_edging(&key.edging));

        let typeface = skia_font.typeface();
        let (font_data, index) = typeface.to_font_data()?;
        // Only the lower 16 bits are part of the index, the rest indicates named instances. But we
        // don't care about those here, since we are just loading the font, so ignore them
        let index = index & 0xFFFF;
        let swash_font = SwashFont::from_data(font_data, index)?;

        Some(Self { key, skia_font, swash_font })
    }
}

impl PartialEq for FontPair {
    fn eq(&self, other: &Self) -> bool {
        self.swash_font.key == other.swash_font.key
    }
}

#[derive(Debug, Default, Hash, PartialEq, Eq, Clone)]
pub struct FontKey {
    // TODO(smolck): Could make these private and add constructor method(s)?
    // Would theoretically make things safer I guess, but not sure . . .
    pub font_desc: Option<FontDescription>,
    pub hinting: FontHinting,
    pub edging: FontEdging,
}

pub struct FontLoader {
    font_mgr: FontMgr,
    cache: LruCache<FontKey, Rc<FontPair>>,
    failed_fonts: HashSet<FontKey>,
    font_size: f32,
    last_resort: Option<Rc<FontPair>>,
}

impl Display for FontKey {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "FontKey {{ font_desc: {:?}, hinting: {:?}, edging: {:?} }}",
            self.font_desc, self.hinting, self.edging
        )
    }
}

impl FontLoader {
    pub fn new(font_size: f32) -> FontLoader {
        FontLoader {
            font_mgr: FontMgr::new(),
            cache: LruCache::new(NonZeroUsize::new(20).unwrap()),
            failed_fonts: HashSet::new(),
            font_size,
            last_resort: None,
        }
    }

    fn load(&mut self, font_key: FontKey) -> Option<FontPair> {
        tracy_zone!("load_font");
        trace!("Loading font {font_key:?}");
        if let Some(desc) = &font_key.font_desc {
            let (family, style) = desc.as_family_and_font_style();
            let typeface = self.font_mgr.match_family_style(family, style)?;
            FontPair::new(font_key, Font::from_typeface(typeface, self.font_size))
        } else {
            let data = Data::new_copy(DEFAULT_FONT);
            let typeface = self.font_mgr.new_from_data(&data, 0)?;
            FontPair::new(font_key, Font::from_typeface(typeface, self.font_size))
        }
    }

    pub fn get_or_load(&mut self, font_key: &FontKey) -> Option<Rc<FontPair>> {
        if let Some(cached) = self.cache.get(font_key) {
            return Some(cached.clone());
        }

        if self.failed_fonts.contains(font_key) {
            return None;
        }

        let font = match self.load(font_key.clone()) {
            Some(loaded_font) => loaded_font,
            None => {
                self.failed_fonts.insert(font_key.clone());
                return None;
            }
        };

        let font = Rc::new(font);
        self.cache.put(font_key.clone(), font.clone());
        self.failed_fonts.remove(font_key);

        Some(font)
    }

    pub fn load_font_for_character(
        &mut self,
        coarse_style: CoarseStyle,
        character: char,
    ) -> Option<Rc<FontPair>> {
        let font_style = coarse_style.into();
        let typeface =
            self.font_mgr.match_family_style_character("", font_style, &[], character as i32)?;

        let font_key = FontKey {
            font_desc: Some(FontDescription {
                family: typeface.family_name(),
                style: coarse_style.name().map(str::to_string),
            }),
            hinting: FontHinting::default(),
            edging: FontEdging::default(),
        };

        let font_pair = Rc::new(FontPair::new(
            font_key.clone(),
            Font::from_typeface(typeface, self.font_size),
        )?);

        self.cache.put(font_key, font_pair.clone());

        Some(font_pair)
    }

    pub fn get_or_load_last_resort(&mut self) -> Option<Rc<FontPair>> {
        if self.last_resort.is_some() {
            self.last_resort.clone()
        } else {
            let font_key = FontKey::default();
            let data = Data::new_copy(LAST_RESORT_FONT);

            let typeface = self.font_mgr.new_from_data(&data, 0)?;
            let font_pair =
                Rc::new(FontPair::new(font_key, Font::from_typeface(typeface, self.font_size))?);

            self.last_resort = Some(font_pair.clone());
            Some(font_pair)
        }
    }

    pub fn loaded_fonts(&self) -> Vec<Rc<FontPair>> {
        self.cache.iter().map(|(_, v)| v.clone()).collect()
    }

    pub fn refresh(&mut self, font_pair: &FontPair) {
        self.cache.get(&font_pair.key);
    }

    pub fn font_names(&self) -> Vec<String> {
        self.font_mgr.family_names().collect()
    }
}

fn font_hinting(hinting: &FontHinting) -> SkiaHinting {
    match hinting {
        FontHinting::Full => SkiaHinting::Full,
        FontHinting::Slight => SkiaHinting::Slight,
        FontHinting::Normal => SkiaHinting::Normal,
        FontHinting::None => SkiaHinting::None,
    }
}

fn font_edging(edging: &FontEdging) -> SkiaEdging {
    match edging {
        FontEdging::AntiAlias => SkiaEdging::AntiAlias,
        FontEdging::Alias => SkiaEdging::Alias,
        FontEdging::SubpixelAntiAlias => SkiaEdging::SubpixelAntiAlias,
    }
}

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

    #[test]
    fn caches_failed_font_keys() {
        let mut loader = FontLoader::new(14.0);
        let missing_font = FontKey {
            font_desc: Some(FontDescription {
                family: "missing-font-family".to_string(),
                style: None,
            }),
            hinting: FontHinting::default(),
            edging: FontEdging::default(),
        };

        assert!(loader.get_or_load(&missing_font).is_none());
        assert!(loader.failed_fonts.contains(&missing_font));

        assert!(loader.get_or_load(&missing_font).is_none());
        assert_eq!(loader.failed_fonts.len(), 1);
    }
}
