use std::{
    collections::HashMap,
    sync::{Arc, Mutex},
};
#[cfg(feature = "image")]
use std::{path::PathBuf, str::FromStr};

#[cfg(feature = "raster")]
use anyhow::Context as AnyhowContext;
use pangocairo::pango;
#[cfg(feature = "svg")]
use resvg::{tiny_skia, usvg};

pub struct FontCache {
    cache: HashMap<String, pango::FontDescription>,
}

impl FontCache {
    pub fn new() -> Self {
        Self {
            cache: HashMap::new(),
        }
    }

    pub fn get(&mut self, font_str: &str) -> &pango::FontDescription {
        self.cache
            .entry(font_str.into())
            .or_insert_with(|| pango::FontDescription::from_string(font_str))
    }
}

#[cfg(feature = "image")]
pub type Image = cairo::ImageSurface;

#[derive(Eq, Hash, Clone, PartialEq, Debug)]
#[cfg(feature = "image")]
pub struct ImageKey {
    file_name: String,
    fit_to_height: u32,
}

#[derive(Clone)]
#[cfg(feature = "image")]
pub struct ImageLoader {
    cache: HashMap<ImageKey, Image>,
}

#[cfg(feature = "image")]
impl ImageLoader {
    #[cfg(any(feature = "raster", feature = "svg"))]
    fn image_from_rgba8(
        buf: &mut [u8],
        width: i32,
        height: i32,
    ) -> anyhow::Result<cairo::ImageSurface> {
        let format = cairo::Format::ARgb32;
        let mut image = cairo::ImageSurface::create(format, width, height)?;
        // rgba => bgra (reverse argb)
        for rgba in buf.chunks_mut(4) {
            rgba.swap(0, 2);
            // Pre-multiplying.
            for i in 0..3 {
                rgba[i] = (rgba[i] as u16 * rgba[3] as u16 / 256) as u8;
            }
        }
        image.data()?.copy_from_slice(buf);
        Ok(image)
    }

    #[cfg(feature = "raster")]
    fn load_raster(file_name: &str, fit_to_height: f64) -> anyhow::Result<cairo::ImageSurface> {
        let img_buf = image::ImageReader::open(file_name)?
            .decode()
            .context("Unable to decode image")?
            .into_rgba8();
        let mut scale = fit_to_height as f32 / img_buf.height() as f32;
        if scale > 1.0 {
            // Do not scale up.
            scale = 1.0;
        }
        let img_buf = if (scale - 1.0).abs() < 0.01 {
            img_buf
        } else {
            image::imageops::resize(
                &img_buf,
                (img_buf.width() as f32 * scale) as u32,
                (img_buf.height() as f32 * scale) as u32,
                image::imageops::FilterType::Triangle,
            )
        };
        let (w, h) = (img_buf.width(), img_buf.height());
        Self::image_from_rgba8(&mut img_buf.into_raw(), w.try_into()?, h.try_into()?)
    }

    #[cfg(feature = "svg")]
    fn load_svg(file_name: &str, fit_to_height: f64) -> anyhow::Result<cairo::ImageSurface> {
        let tree = {
            let mut opt = usvg::Options {
                resources_dir: std::fs::canonicalize(file_name)
                    .ok()
                    .and_then(|p| p.parent().map(|p| p.to_path_buf())),
                ..Default::default()
            };
            opt.fontdb_mut().load_system_fonts();
            let svg_data = std::fs::read(file_name).unwrap();
            usvg::Tree::from_data(&svg_data, &opt).unwrap()
        };
        let size = tree.size().to_int_size(); // cannot be zero.
        let mut scale = fit_to_height as f32 / size.height() as f32;
        if scale > 1.0 {
            // Do not scale up.
            scale = 1.0;
        }
        let (w, h) = (size.width() as f32 * scale, size.height() as f32 * scale);
        let mut pixmap = tiny_skia::Pixmap::new(w as u32, h as u32).unwrap();
        resvg::render(
            &tree,
            tiny_skia::Transform::from_scale(scale, scale),
            &mut pixmap.as_mut(),
        );
        Self::image_from_rgba8(pixmap.data_mut(), w as i32, h as i32)
    }

    fn do_load_image(&self, file_name: &str, fit_to_height: f64) -> anyhow::Result<Image> {
        match PathBuf::from_str(file_name)?.extension() {
            #[cfg(feature = "svg")]
            Some(s) if s == "svg" => Self::load_svg(file_name, fit_to_height),
            #[cfg(feature = "raster")]
            _ => Self::load_raster(file_name, fit_to_height),
            #[cfg(not(feature = "raster"))]
            ext => Err(anyhow::anyhow!(
                "Image format support for {} is not enabled",
                ext.map(|s| s.to_string_lossy())
                    .unwrap_or("<no extension>".into())
            )),
        }
    }

    pub fn load_image(
        &mut self,
        file_name: &str,
        fit_to_height: f64,
        cache_images: bool,
    ) -> anyhow::Result<Image> {
        let key = ImageKey {
            file_name: file_name.into(),
            fit_to_height: fit_to_height as u32,
        };
        if cache_images {
            if let Some(image) = self.cache.get(&key) {
                tracing::debug!("Got {:?} from cache", key);
                return Ok(image.clone());
            }
            tracing::debug!("{:?} not in cache, loading...", key);
            let image = self.do_load_image(file_name, fit_to_height)?;
            self.cache.insert(key, image.clone());
            Ok(image)
        } else {
            tracing::debug!("Cache disabled, loading {:?}...", key);
            self.do_load_image(file_name, fit_to_height)
        }
    }

    pub fn load_from_argb_pixmap(
        &mut self,
        width: i32,
        height: i32,
        argb_data: &[u64],
        fit_to_height: f64,
    ) -> anyhow::Result<Image> {
        let format = cairo::Format::ARgb32;
        let mut image = cairo::ImageSurface::create(format, width, height)?;
        {
            let mut data = image.data()?;
            for (dst, src) in data.chunks_mut(4).zip(argb_data.chunks(4)) {
                if src.len() < 4 {
                    break;
                }
                let a = src[0];
                let r = src[1];
                let g = src[2];
                let b = src[3];
                // Pre-multiply and store as BGRA (native LE for Cairo ARgb32).
                dst[0] = (b as u16 * a as u16 / 255) as u8;
                dst[1] = (g as u16 * a as u16 / 255) as u8;
                dst[2] = (r as u16 * a as u16 / 255) as u8;
                dst[3] = a as u8;
            }
        }
        // Scale down if needed (don't scale up).
        let scale = (fit_to_height / height as f64).min(1.0);
        if (scale - 1.0).abs() < 0.01 {
            return Ok(image);
        }
        let new_w = (width as f64 * scale) as i32;
        let new_h = (height as f64 * scale) as i32;
        let scaled = cairo::ImageSurface::create(format, new_w, new_h)?;
        let cr = cairo::Context::new(&scaled)?;
        cr.scale(scale, scale);
        cr.set_source_surface(&image, 0.0, 0.0)?;
        cr.paint()?;
        drop(cr);
        Ok(scaled)
    }

    /// Load an icon by name using GTK4's IconTheme.
    /// Renders the IconPaintable directly to a Cairo surface via GtkSnapshot.
    /// Optionally accepts an icon_theme_path to add as a search directory.
    #[cfg(feature = "gtk4_icons")]
    pub fn load_from_icon_name(
        &mut self,
        icon_name: &str,
        icon_theme_path: &str,
        fit_to_height: f64,
        cache: bool,
    ) -> anyhow::Result<Image> {
        use anyhow::Context;
        use gtk4::prelude::*;

        let key = ImageKey {
            file_name: format!("icon:{}:{}", icon_name, icon_theme_path),
            fit_to_height: fit_to_height as u32,
        };
        if cache {
            if let Some(image) = self.cache.get(&key) {
                return Ok(image.clone());
            }
        }
        let display = gdk4::Display::default().unwrap();
        let icon_theme = gtk4::IconTheme::for_display(&display);
        if !icon_theme_path.is_empty() {
            let icon_theme_path = PathBuf::from(icon_theme_path);
            icon_theme.set_search_path(&[&icon_theme_path]);
        }

        let size = fit_to_height as i32;
        let paintable = icon_theme.lookup_icon(
            icon_name,
            &[],
            size,
            1,
            gtk4::TextDirection::Ltr,
            gtk4::IconLookupFlags::FORCE_REGULAR,
        );

        let width = paintable.intrinsic_width().max(size);
        let height = paintable.intrinsic_height().max(size);

        let snapshot = gtk4::Snapshot::new();
        paintable.snapshot(&snapshot, width as f64, height as f64);
        let node = snapshot
            .to_node()
            .with_context(|| format!("Failed to render icon {:?} to node", icon_name))?;

        let scale = (fit_to_height / height as f64).min(1.0);
        let target_w = (width as f64 * scale) as i32;
        let target_h = (height as f64 * scale) as i32;

        let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, target_w, target_h)?;
        let cr = cairo::Context::new(&surface)?;
        cr.scale(scale, scale);
        node.draw(&cr);
        drop(cr);

        if cache {
            self.cache.insert(key, surface.clone());
        }
        Ok(surface)
    }

    pub fn new() -> Self {
        Self {
            cache: HashMap::new(),
        }
    }
}

#[derive(PartialEq, Eq, Clone)]
pub enum Mode {
    Full,
    Shape,
}

#[derive(Clone)]
pub struct Context {
    pub context: cairo::Context,
    pub pango_context: Option<pango::Context>,
    pub mode: Mode,
    pub font_cache: Arc<Mutex<FontCache>>,
    #[cfg(feature = "image")]
    pub image_loader: ImageLoader,
    pub pointer_position: Option<(i16, i16)>,
    pub hover: bool,
}

pub struct Color {
    pub r: f64,
    pub g: f64,
    pub b: f64,
    pub a: f64,
}

impl Color {
    pub fn parse(color: &str) -> anyhow::Result<Self> {
        let (pango_color, alpha) = pango::Color::parse_with_alpha(color)?;
        let scale = 65536.0;
        Ok(Self {
            r: pango_color.red() as f64 / scale,
            g: pango_color.green() as f64 / scale,
            b: pango_color.blue() as f64 / scale,
            a: alpha as f64 / scale,
        })
    }
}

impl Context {
    pub fn new(
        context: cairo::Context,
        font_cache: Arc<Mutex<FontCache>>,
        #[cfg(feature = "image")] image_loader: ImageLoader,
        mode: Mode,
    ) -> anyhow::Result<Self> {
        context.set_antialias(cairo::Antialias::Fast);
        context.set_line_join(cairo::LineJoin::Round);
        context.set_line_cap(cairo::LineCap::Square);
        let pango_context = match mode {
            Mode::Full => Some(pangocairo::functions::create_context(&context)),
            Mode::Shape => None,
        };
        Ok(Self {
            font_cache,
            #[cfg(feature = "image")]
            image_loader,
            context,
            pango_context,
            mode,
            pointer_position: None,
            hover: false,
        })
    }

    pub fn set_source_color(&self, color: Color) {
        self.context
            .set_source_rgba(color.r, color.g, color.b, color.a);
    }

    pub fn set_source_rgba(&self, color: &str) -> anyhow::Result<()> {
        if color.is_empty() {
            return Ok(());
        }
        match Color::parse(color) {
            Ok(color) => {
                self.set_source_color(color);
                Ok(())
            }
            Err(e) => Err(anyhow::anyhow!(
                "failed to parse color: {:?}, err={:?}",
                color,
                e
            )),
        }
    }

    pub fn set_source_rgba_background(&self, color: &str) -> anyhow::Result<()> {
        if color.is_empty() {
            return Ok(());
        }
        match Color::parse(color) {
            Ok(color) if self.mode == Mode::Shape => {
                self.set_source_color(Color {
                    r: 0.0,
                    g: 0.0,
                    b: 0.0,
                    a: if color.a == 0.0 { 0.0 } else { 1.0 },
                });
                Ok(())
            }
            Ok(color) => {
                self.set_source_color(color);
                Ok(())
            }
            Err(e) => Err(anyhow::anyhow!(
                "failed to parse color: {:?}, err={:?}",
                color,
                e
            )),
        }
    }
}
