use std::{
    cell::RefCell,
    ops::Deref,
    rc::{Rc, Weak},
};
use wayland_client::{
    protocol::{wl_compositor, wl_pointer, wl_seat, wl_shm, wl_surface},
    Attached, DispatchData,
};
use wayland_cursor::{Cursor, CursorTheme};

/// The specification of a cursor theme to be used by the ThemeManager
pub enum ThemeSpec<'a> {
    /// Use this specific theme with given base size
    Precise {
        /// Name of the cursor theme to use
        name: &'a str,
        /// Base size of the cursor images
        ///
        /// This is the size that will be used on monitors with a scale
        /// factor of 1. Cursor images sizes will be multiples of this
        /// base size on HiDPI outputs.
        size: u32,
    },
    /// Use the system provided theme
    ///
    /// In this case SCTK will read the `XCURSOR_THEME` and
    /// `XCURSOR_SIZE` environment variables to figure out the
    /// theme to use.
    System,
}

/// Wrapper managing a system theme for pointer images
///
/// You can use it to initialize new pointers in order
/// to theme them.
///
/// Is is also clone-able in case you need to handle several
/// pointer theming from different places.
///
/// Note that it is however neither `Send` nor `Sync`
#[derive(Clone)]
pub struct ThemeManager {
    themes: Rc<RefCell<ScaledThemeList>>,
    compositor: Attached<wl_compositor::WlCompositor>,
}

impl ThemeManager {
    /// Load a system pointer theme
    ///
    /// Will use the default theme of the system if name is `None`.
    pub fn init(
        theme: ThemeSpec,
        compositor: Attached<wl_compositor::WlCompositor>,
        shm: Attached<wl_shm::WlShm>,
    ) -> ThemeManager {
        ThemeManager { compositor, themes: Rc::new(RefCell::new(ScaledThemeList::new(theme, shm))) }
    }

    /// Wrap a pointer to theme it
    pub fn theme_pointer(&self, pointer: wl_pointer::WlPointer) -> ThemedPointer {
        let surface = self.compositor.create_surface();
        let inner = Rc::new(RefCell::new(PointerInner {
            surface: surface.detach(),
            themes: self.themes.clone(),
            last_serial: 0,
            current_cursor: "left_ptr".into(),
            scale_factor: 1,
        }));
        let my_pointer = pointer.clone();
        let winner = Rc::downgrade(&inner);
        crate::surface::setup_surface(
            surface,
            Some(move |scale_factor, _, _: DispatchData| {
                if let Some(inner) = Weak::upgrade(&winner) {
                    let mut inner = inner.borrow_mut();
                    inner.scale_factor = scale_factor;
                    // we can't handle errors here, so ignore it
                    // worst that can happen is cursor drawn with the wrong
                    // scale factor
                    let _ = inner.update_cursor(&my_pointer);
                }
            }),
        );
        ThemedPointer { pointer, inner }
    }

    /// Initialize a new pointer as a ThemedPointer with an adapter implementation
    ///
    /// You need to provide an implementation as if implementing a `wl_pointer`, but
    /// it will receive as `meta` argument a `ThemedPointer` wrapping your pointer,
    /// rather than a `WlPointer`.
    pub fn theme_pointer_with_impl<F>(
        &self,
        seat: &Attached<wl_seat::WlSeat>,
        mut callback: F,
    ) -> ThemedPointer
    where
        F: FnMut(wl_pointer::Event, ThemedPointer, DispatchData) + 'static,
    {
        let surface = self.compositor.create_surface();
        let inner = Rc::new(RefCell::new(PointerInner {
            surface: surface.detach(),
            themes: self.themes.clone(),
            last_serial: 0,
            current_cursor: "left_ptr".into(),
            scale_factor: 1,
        }));

        let inner2 = inner.clone();
        let pointer = seat.get_pointer();
        pointer.quick_assign(move |ptr, event, ddata| {
            callback(event, ThemedPointer { pointer: ptr.detach(), inner: inner2.clone() }, ddata)
        });

        let winner = Rc::downgrade(&inner);
        let my_pointer = pointer.clone();
        crate::surface::setup_surface(
            surface,
            Some(move |scale_factor, _, _: DispatchData| {
                if let Some(inner) = Weak::upgrade(&winner) {
                    let mut inner = inner.borrow_mut();
                    inner.scale_factor = scale_factor;
                    // we can't handle errors here, so ignore it
                    // worst that can happen is cursor drawn with the wrong
                    // scale factor
                    let _ = inner.update_cursor(&my_pointer);
                }
            }),
        );

        ThemedPointer { pointer: pointer.detach(), inner }
    }
}

struct ScaledThemeList {
    shm: Attached<wl_shm::WlShm>,
    name: String,
    size: u32,
    themes: Vec<(u32, CursorTheme)>,
}

impl ScaledThemeList {
    fn new(theme: ThemeSpec, shm: Attached<wl_shm::WlShm>) -> ScaledThemeList {
        let (name, size) = match theme {
            ThemeSpec::Precise { name, size } => (name.into(), size),
            ThemeSpec::System => {
                let name = std::env::var("XCURSOR_THEME").ok().unwrap_or_else(|| "default".into());
                let size =
                    std::env::var("XCURSOR_SIZE").ok().and_then(|s| s.parse().ok()).unwrap_or(24);
                (name, size)
            }
        };
        ScaledThemeList { shm, name, size, themes: vec![] }
    }

    fn get_cursor(&mut self, name: &str, scale: u32) -> Option<&Cursor> {
        // Check if we already loaded the theme for this scale factor
        let opt_index = self.themes.iter().position(|&(s, _)| s == scale);
        if let Some(idx) = opt_index {
            self.themes[idx].1.get_cursor(name)
        } else {
            let new_theme = CursorTheme::load_from_name(&self.name, self.size * scale, &self.shm);
            self.themes.push((scale, new_theme));
            self.themes.last_mut().unwrap().1.get_cursor(name)
        }
    }
}

struct PointerInner {
    surface: wl_surface::WlSurface,
    themes: Rc<RefCell<ScaledThemeList>>,
    current_cursor: String,
    last_serial: u32,
    scale_factor: i32,
}

impl PointerInner {
    fn update_cursor(&self, pointer: &wl_pointer::WlPointer) -> Result<(), ()> {
        let mut themes = self.themes.borrow_mut();
        let scale = self.scale_factor as u32;
        let cursor = themes.get_cursor(&self.current_cursor, scale).ok_or(())?;
        let image = &cursor[0];
        let (w, h) = image.dimensions();
        let (hx, hy) = image.hotspot();
        self.surface.set_buffer_scale(scale as i32);
        self.surface.attach(Some(&image), 0, 0);
        if self.surface.as_ref().version() >= 4 {
            self.surface.damage_buffer(0, 0, w as i32, h as i32);
        } else {
            // surface is old and does not support damage_buffer, so we damage
            // in surface coordinates and hope it is not rescaled
            self.surface.damage(0, 0, w as i32 / scale as i32, h as i32 / scale as i32);
        }
        self.surface.commit();
        pointer.set_cursor(
            self.last_serial,
            Some(&self.surface),
            hx as i32 / scale as i32,
            hy as i32 / scale as i32,
        );
        Ok(())
    }
}

/// Wrapper of a themed pointer
///
/// You can access the underlying `wl_pointer::WlPointer` via
/// deref. It will *not* release the proxy when dropped.
///
/// Just like `Proxy`, this is a `Rc`-like wrapper. You can clone it
/// to have several handles to the same theming machinery of a pointer.
#[derive(Clone)]
pub struct ThemedPointer {
    pointer: wl_pointer::WlPointer,
    inner: Rc<RefCell<PointerInner>>,
}

impl ThemedPointer {
    /// Change the cursor to the given cursor name
    ///
    /// Possible names depend on the theme. Does nothing and returns
    /// `Err(())` if given name is not available.
    ///
    /// If this is done as an answer to an input event, you need to provide
    /// the associated serial otherwise the server may ignore the request.
    pub fn set_cursor(&self, name: &str, serial: Option<u32>) -> Result<(), ()> {
        let mut inner = self.inner.borrow_mut();
        if let Some(s) = serial {
            inner.last_serial = s;
        }
        inner.current_cursor = name.into();
        inner.update_cursor(&self.pointer)
    }
}

impl Deref for ThemedPointer {
    type Target = wl_pointer::WlPointer;
    fn deref(&self) -> &wl_pointer::WlPointer {
        &self.pointer
    }
}

impl Drop for PointerInner {
    fn drop(&mut self) {
        self.surface.destroy();
    }
}
