use std::collections::HashMap;
use std::path::PathBuf;

use cursor_icon::CursorIcon;
use smithay::utils::{Logical, Point};
use tracing::warn;
use xcursor::parser::{parse_xcursor, Image};

/// Nominal cursor size in logical pixels, scaled by the output scale.
const BASE_SIZE: u32 = 24;

/// What to draw for the pointer this frame: where, and which shape.
#[derive(Clone, Copy, Debug)]
pub struct CursorRender {
    pub position: Point<f64, Logical>,
    pub icon: CursorIcon,
}

/// One decoded XCursor frame: RGBA pixels plus the hotspot in buffer pixels.
pub struct ThemedCursor {
    pub pixels: Vec<u8>,
    pub width: i32,
    pub height: i32,
    pub xhot: i32,
    pub yhot: i32,
}

/// Loads pointer cursors from the active ltk theme's `cursors/` directory,
/// in XCursor format. Parsed images are cached per cursor name.
pub struct CursorTheme {
    dir: Option<PathBuf>,
    cache: HashMap<&'static str, Option<Vec<Image>>>,
}

impl std::fmt::Debug for CursorTheme {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("CursorTheme").field("dir", &self.dir).finish()
    }
}

impl CursorTheme {
    pub fn load() -> Self {
        let dir = resolve_dir();
        if dir.is_none() {
            warn!("no ltk cursor theme found; falling back to built-in cursors");
        }
        Self {
            dir,
            cache: HashMap::new(),
        }
    }

    /// Returns the cursor for `icon` at the given output `scale`, or `None`
    /// when the theme has no such cursor (the caller draws a built-in shape).
    pub fn image(&mut self, icon: CursorIcon, scale: f64) -> Option<ThemedCursor> {
        let target = (BASE_SIZE as f64 * scale).round().max(1.0) as i64;
        let images = self.images(icon.name())?;
        let img = images
            .iter()
            .min_by_key(|i| (target - i.size as i64).abs())?;
        Some(ThemedCursor {
            pixels: img.pixels_rgba.clone(),
            width: img.width as i32,
            height: img.height as i32,
            xhot: img.xhot as i32,
            yhot: img.yhot as i32,
        })
    }

    fn images(&mut self, name: &'static str) -> Option<&Vec<Image>> {
        if !self.cache.contains_key(name) {
            let parsed = self.parse(name);
            self.cache.insert(name, parsed);
        }
        self.cache.get(name).and_then(|o| o.as_ref())
    }

    fn parse(&self, name: &str) -> Option<Vec<Image>> {
        let data = std::fs::read(self.dir.as_ref()?.join(name)).ok()?;
        parse_xcursor(&data)
    }
}

/// Mirrors ltk's theme search order and appends the active theme's `cursors/`
/// subdirectory, returning the first that exists.
fn resolve_dir() -> Option<PathBuf> {
    let id = ltk::theme::active_theme_id();
    ltk::theme::search::search_paths()
        .into_iter()
        .map(|base| base.join(&id).join("cursors"))
        .find(|dir| dir.is_dir())
}
