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

use cursor_icon::CursorIcon;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
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;

/// Where the pointer imagery comes from: a shape resolved against the theme,
/// or a surface the client attached through `wl_pointer.set_cursor`.
#[derive(Clone, Debug)]
pub enum CursorImage {
    Named(CursorIcon),
    Surface {
        surface: WlSurface,
        hotspot: Point<i32, Logical>,
    },
}

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

/// One decoded XCursor frame. `pixels` keeps the file's byte order — a
/// little-endian ARGB `u32`, which is what `Fourcc::Argb8888` names.
pub struct CursorFrame {
    pub pixels: Vec<u8>,
    pub width: i32,
    pub height: i32,
    pub xhot: i32,
    pub yhot: i32,
    pub delay_ms: u32,
}

/// Every frame of one shape at one nominal size. Static shapes hold exactly one.
pub struct ThemedCursor {
    pub frames: Vec<CursorFrame>,
}

/// 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>,
    theme_id: String,
    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 theme_id = ltk::theme::active_theme_id();
        let dir = resolve_dir(&theme_id);
        if dir.is_none() {
            warn!(
                "ltk theme '{theme_id}' has no cursors/ directory; no pointer will be drawn \
                 (is the ltk-theme package installed and loadable?)"
            );
        }
        Self {
            dir,
            theme_id,
            cache: HashMap::new(),
        }
    }

    /// Re-resolve when the active ltk theme changes. Reports whether it did,
    /// so the caller can drop its own derived textures. The light/dark mode is
    /// deliberately not part of the key: `cursors/` has no per-mode variants.
    pub fn tick(&mut self) -> bool {
        let id = ltk::theme::active_theme_id();
        if id == self.theme_id {
            return false;
        }
        self.dir = resolve_dir(&id);
        self.theme_id = id;
        self.cache.clear();
        true
    }

    /// Returns every frame of `icon` at the size closest to the output `scale`,
    /// or `None` when the theme has no such cursor.
    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())?;

        // Images are drawn 1:1, never resampled, so the size quantises to the
        // set the file carries. Ties go to the larger one: a slightly big
        // pointer stays usable, a small one on a HiDPI panel does not.
        let size = images
            .iter()
            .map(|i| i.size)
            .min_by_key(|s| ((target - *s as i64).abs(), std::cmp::Reverse(*s)))?;

        let frames: Vec<CursorFrame> = images
            .iter()
            .filter(|i| i.size == size)
            .map(|i| CursorFrame {
                pixels: i.pixels_rgba.clone(),
                width: i.width as i32,
                height: i.height as i32,
                xhot: i.xhot as i32,
                yhot: i.yhot as i32,
                delay_ms: i.delay,
            })
            .collect();

        (!frames.is_empty()).then_some(ThemedCursor { frames })
    }

    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 theme's `cursors/`
/// subdirectory, returning the first that exists.
fn resolve_dir(theme_id: &str) -> Option<PathBuf> {
    ltk::theme::search::search_paths()
        .into_iter()
        .map(|base| base.join(theme_id).join("cursors"))
        .find(|dir| dir.is_dir())
}
