use crate::WINGsP::WMScreen;
use std::cell::RefCell;
use std::ffi::c_ulong;
use std::ptr::{self, NonNull};
use wrlib_rs::ffi::{RColor, RGetClosestXColor};

pub struct Color {
    screen: NonNull<WMScreen>,
    color: x11::xlib::XColor,
    alpha: RefCell<u16>,
    gc: RefCell<x11::xlib::GC>,
}

#[derive(Clone, Copy, Debug)]
pub struct Rgb {
    pub r: u16,
    pub g: u16,
    pub b: u16,
}

impl Rgb {
    pub fn with_alpha(self, alpha: u16) -> Rgba {
        Rgba {
            r: self.r,
            g: self.g,
            b: self.b,
            a: alpha,
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ColorMatch {
    Approximate,
    Exact,
}

#[derive(Clone, Copy, Debug)]
pub struct Rgba {
    pub r: u16,
    pub g: u16,
    pub b: u16,
    pub a: u16,
}

impl Color {
    const LIGHT_STIPPLE_WIDTH: u32 = 4;
    const LIGHT_STIPPLE_HEIGHT: u32 = 4;
    const LIGHT_STIPPLE_BITS: [u8; 4] = [0x05, 0x0a, 0x05, 0x0a];
    const DARK_STIPPLE_WIDTH: u32 = 4;
    const DARK_STIPPLE_HEIGHT: u32 = 4;
    const DARK_STIPPLE_BITS: [u8; 4] = [0x0a, 0x04, 0x0a, 0x01];

    pub fn new_rgb(screen: NonNull<WMScreen>, rgb: Rgb, mtch: ColorMatch) -> Option<Self> {
        Self::new_rgba(screen, rgb.with_alpha(0xffff), mtch)
    }

    pub fn new_rgba(screen: NonNull<WMScreen>, rgba: Rgba, mtch: ColorMatch) -> Option<Self> {
        match mtch {
            ColorMatch::Approximate => find_close_rgba(screen, rgba),
            ColorMatch::Exact => create_rgba(screen, rgba),
        }
    }

    pub fn red(&self) -> u16 {
        self.color.red
    }

    pub fn green(&self) -> u16 {
        self.color.green
    }

    pub fn blue(&self) -> u16 {
        self.color.blue
    }

    pub fn alpha(&self) -> u16 {
        *self.alpha.borrow()
    }

    pub fn pixel(&self) -> c_ulong {
        self.color.pixel
    }

    pub fn to_xft_color(&self) -> x11::xft::XftColor {
        x11::xft::XftColor {
            color: x11::xrender::XRenderColor {
                red: self.red(),
                green: self.green(),
                blue: self.blue(),
                alpha: self.alpha(),
            },
            pixel: self.pixel(),
        }
    }

    pub fn to_rcolor(&self) -> RColor {
        RColor {
            red: (self.red() >> 8) as u8,
            green: (self.green() >> 8) as u8,
            blue: (self.blue() >> 8) as u8,
            alpha: (self.alpha() >> 8) as u8,
        }
    }

    pub fn gc(&self) -> x11::xlib::GC {
        let mut gc = self.gc.borrow_mut();
        if gc.is_null() {
            let mut gcv =
                unsafe { std::mem::MaybeUninit::<x11::xlib::XGCValues>::zeroed().assume_init() };
            gcv.foreground = self.pixel();
            gcv.graphics_exposures = 0;

            *gc = unsafe {
                let screen = &mut *self.screen.as_ptr();
                x11::xlib::XCreateGC(
                    screen.display,
                    (*screen.rcontext).drawable,
                    (x11::xlib::GCForeground | x11::xlib::GCGraphicsExposures) as u64,
                    &mut gcv,
                )
            };
        }
        *gc
    }

    pub fn hex_triplet(&self) -> String {
        format!(
            "#{:02x}{:02x}{:02x}",
            self.red() >> 8,
            self.green() >> 8,
            self.blue() >> 8
        )
    }
}

/*
 * TODO: make the color creation code return the same WMColor for the
 * same colors.
 * make findCloseColor() find the closest color in the RContext pallette
 * or in the other colors allocated by WINGs.
 */

fn find_close_rgba(screen: NonNull<WMScreen>, rgba: Rgba) -> Option<Color> {
    let rcontext = unsafe { (*screen.as_ptr()).rcontext };
    let display = unsafe { (*screen.as_ptr()).display };
    let colormap = unsafe { (*screen.as_ptr()).colormap };

    let mut xcolor = x11::xlib::XColor {
        pixel: 0,
        red: 0,
        green: 0,
        blue: 0,
        flags: 0,
        pad: 0,
    };
    let mut rcolor = RColor {
        red: (rgba.r >> 8) as u8,
        green: (rgba.g >> 8) as u8,
        blue: (rgba.b >> 8) as u8,
        alpha: (rgba.a >> 8) as u8,
    };
    if unsafe { RGetClosestXColor(rcontext, &mut rcolor, &mut xcolor) } == 0 {
        return None;
    }
    if unsafe { x11::xlib::XAllocColor(display, colormap, &mut xcolor) } == 0 {
        return None;
    }

    Some(Color {
        screen: screen,
        color: xcolor,
        alpha: RefCell::new(rgba.a),
        gc: RefCell::new(ptr::null_mut()),
    })
}

fn create_rgba(screen: NonNull<WMScreen>, rgba: Rgba) -> Option<Color> {
    let display = unsafe { (*screen.as_ptr()).display };
    let colormap = unsafe { (*screen.as_ptr()).colormap };
    let mut xcolor = x11::xlib::XColor {
        red: rgba.r,
        green: rgba.g,
        blue: rgba.b,
        flags: x11::xlib::DoRed | x11::xlib::DoGreen | x11::xlib::DoBlue,
        pad: 0,
        pixel: 0,
    };
    if unsafe { x11::xlib::XAllocColor(display, colormap, &mut xcolor) } == 0 {
        return None;
    }

    Some(Color {
        screen: screen,
        color: xcolor,
        alpha: RefCell::new(rgba.a),
        gc: RefCell::new(ptr::null_mut()),
    })
}

impl Drop for Color {
    fn drop(&mut self) {
        unsafe {
            let display = (*self.screen.as_ptr()).display;
            let colormap = (*self.screen.as_ptr()).colormap;
            x11::xlib::XFreeColors(display, colormap, &mut self.color.pixel, 1, 0);
            let gc = self.gc.borrow();
            if !gc.is_null() {
                x11::xlib::XFreeGC(display, *gc);
            }
        }
    }
}

pub mod ffi {
    use super::*;
    use std::ffi::{c_char, c_int, c_uint, c_ulong, c_ushort, CString};
    use std::ptr::NonNull;
    use std::rc::Rc;
    use wrlib_rs::ffi::RColor;

    pub type WMColor = Rc<Color>;

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMCreateRGBColor(
        screen: NonNull<WMScreen>,
        red: c_ushort,
        green: c_ushort,
        blue: c_ushort,
        exact: c_int,
    ) -> *mut WMColor {
        Color::new_rgb(
            screen,
            Rgb {
                r: red,
                g: green,
                b: blue,
            },
            if exact == 0 {
                ColorMatch::Approximate
            } else {
                ColorMatch::Exact
            },
        )
        .map(|c| Box::leak(Box::new(Rc::new(c))) as *mut _)
        .unwrap_or(unsafe { (*screen.as_ptr()).black })
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMGetRColorFroMColor(color: NonNull<WMColor>) -> RColor {
        unsafe { (*color.as_ptr()).to_rcolor() }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn pixel(c: &WMColor) -> c_ulong {
        c.color.pixel
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMCreateRGBAColor(
        screen: NonNull<WMScreen>,
        red: c_ushort,
        green: c_ushort,
        blue: c_ushort,
        alpha: c_ushort,
        exact: c_int,
    ) -> *mut WMColor {
        Color::new_rgba(
            screen,
            Rgba {
                r: red,
                g: green,
                b: blue,
                a: alpha,
            },
            if exact == 0 {
                ColorMatch::Approximate
            } else {
                ColorMatch::Exact
            },
        )
        .map(|c| Box::leak(Box::new(Rc::new(c))) as *mut _)
        .unwrap_or(unsafe { (*screen.as_ptr()).black })
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMCreateNamedColor(
        screen: NonNull<WMScreen>,
        name: *const c_char,
        exact: c_int,
    ) -> *mut WMColor {
        let display = unsafe { (*screen.as_ptr()).display };
        let colormap = unsafe { (*screen.as_ptr()).colormap };
        let mut xcolor = x11::xlib::XColor {
            red: 0,
            green: 0,
            blue: 0,
            flags: 0,
            pad: 0,
            pixel: 0,
        };
        if unsafe { x11::xlib::XParseColor(display, colormap, name, &mut xcolor) } == 0 {
            return ptr::null_mut();
        }
        let visual_class = unsafe { (*(*screen.as_ptr()).visual).class };
        let exact = visual_class == x11::xlib::TrueColor || exact != 0;
        if exact {
            if let Some(c) = Color::new_rgb(
                screen,
                Rgb {
                    r: xcolor.red,
                    g: xcolor.green,
                    b: xcolor.blue,
                },
                ColorMatch::Exact,
            ) {
                return Box::leak(Box::new(Rc::new(c))) as *mut _;
            }
        }
        match Color::new_rgb(
            screen,
            Rgb {
                r: xcolor.red,
                g: xcolor.green,
                b: xcolor.blue,
            },
            ColorMatch::Approximate,
        ) {
            Some(c) => Box::leak(Box::new(Rc::new(c))) as *mut _,
            None => ptr::null_mut(),
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMRetainColor(color: NonNull<WMColor>) -> *mut WMColor {
        Box::leak(Box::new(unsafe { (*color.as_ptr()).clone() })) as *mut _
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMReleaseColor(color: NonNull<WMColor>) {
        unsafe {
            let _ = Box::from_raw(color.as_ptr());
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMSetColorAlpha(color: NonNull<WMColor>, alpha: c_ushort) {
        unsafe {
            (&mut *color.as_ptr()).alpha.replace(alpha);
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMPaintColorSwatch(
        color: NonNull<WMColor>,
        d: x11::xlib::Drawable,
        x: c_int,
        y: c_int,
        width: c_uint,
        height: c_uint,
    ) {
        unsafe {
            let color = &mut *color.as_ptr();
            let display = (*color.screen.as_ptr()).display;
            x11::xlib::XFillRectangle(display, d, color.gc(), x, y, width, height);
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMColorPixel(color: NonNull<WMColor>) -> c_ulong {
        unsafe { (*color.as_ptr()).pixel() }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMColorGC(color: NonNull<WMColor>) -> x11::xlib::GC {
        unsafe { (&mut *color.as_ptr()).gc() }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMSetColorInGC(color: NonNull<WMColor>, gc: x11::xlib::GC) {
        unsafe {
            let color = &mut *color.as_ptr();
            let display = (*color.screen.as_ptr()).display;
            x11::xlib::XSetForeground(display, gc, color.pixel());
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMWhiteColor(screen: NonNull<WMScreen>) -> *mut WMColor {
        unsafe {
            let scr = &mut *screen.as_ptr();
            if scr.white.is_null() {
                // TODO: warn if we couldn't allocate.
                scr.white = WMCreateRGBColor(screen, 0xffff, 0xffff, 0xffff, 1);
            }

            NonNull::new(scr.white)
                .map(|c| WMRetainColor(c))
                .unwrap_or(ptr::null_mut())
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMBlackColor(screen: NonNull<WMScreen>) -> *mut WMColor {
        unsafe {
            let scr = &mut *screen.as_ptr();
            if scr.black.is_null() {
                // TODO: warn or bail out if we couldn't allocate.
                scr.black = WMCreateRGBColor(screen, 0, 0, 0, 1);
            }

            NonNull::new(scr.black)
                .map(|c| WMRetainColor(c))
                .unwrap_or(ptr::null_mut())
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMGrayColor(screen: NonNull<WMScreen>) -> *mut WMColor {
        unsafe {
            let scr = &mut *screen.as_ptr();
            if scr.gray.is_null() {
                if scr.depth == 1 {
                    let white = WMWhiteColor(screen);
                    let black = WMBlackColor(screen);
                    let stipple = x11::xlib::XCreateBitmapFromData(
                        scr.display,
                        (*scr.rcontext).drawable,
                        Color::LIGHT_STIPPLE_BITS.as_ptr(),
                        Color::LIGHT_STIPPLE_WIDTH,
                        Color::LIGHT_STIPPLE_HEIGHT,
                    );
                    let color = create_rgba(
                        screen,
                        Rgba {
                            r: 0,
                            g: 0,
                            b: 0,
                            a: 0xffff,
                        },
                    )
                    .expect("cannot create stipple color");
                    let mut gcv =
                        std::mem::MaybeUninit::<x11::xlib::XGCValues>::zeroed().assume_init();
                    gcv.foreground = (&mut *white).color.pixel;
                    gcv.background = (&mut *black).color.pixel;
                    gcv.fill_style = x11::xlib::FillStippled;
                    gcv.stipple = stipple;
                    *color.gc.borrow_mut() = x11::xlib::XCreateGC(
                        scr.display,
                        (*scr.rcontext).drawable,
                        (x11::xlib::GCForeground
                            | x11::xlib::GCBackground
                            | x11::xlib::GCStipple
                            | x11::xlib::GCFillStyle
                            | x11::xlib::GCGraphicsExposures) as u64,
                        &mut gcv,
                    );
                    x11::xlib::XFreePixmap(scr.display, stipple);
                    if let Some(white) = NonNull::new(white) {
                        WMReleaseColor(white);
                    }
                    if let Some(black) = NonNull::new(black) {
                        WMReleaseColor(black);
                    }
                } else {
                    scr.gray = WMCreateRGBColor(screen, 0xaeba, 0xaaaa, 0xaeba, 1);
                    // TODO: warn or bail if we couldn't allocate.
                }
            }
            NonNull::new(scr.gray)
                .map(|c| WMRetainColor(c))
                .unwrap_or(ptr::null_mut())
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMDarkGrayColor(screen: NonNull<WMScreen>) -> *mut WMColor {
        unsafe {
            let scr = &mut *screen.as_ptr();
            if scr.darkGray.is_null() {
                if scr.depth == 1 {
                    let white = WMWhiteColor(screen);
                    let black = WMBlackColor(screen);
                    let stipple = x11::xlib::XCreateBitmapFromData(
                        scr.display,
                        (*scr.rcontext).drawable,
                        Color::DARK_STIPPLE_BITS.as_ptr(),
                        Color::DARK_STIPPLE_WIDTH,
                        Color::DARK_STIPPLE_HEIGHT,
                    );
                    let color = create_rgba(
                        screen,
                        Rgba {
                            r: 0,
                            g: 0,
                            b: 0,
                            a: 0xffff,
                        },
                    )
                    .expect("cannot create dark stipple color");
                    let mut gcv =
                        std::mem::MaybeUninit::<x11::xlib::XGCValues>::zeroed().assume_init();
                    gcv.foreground = (&mut *white).color.pixel;
                    gcv.background = (&mut *black).color.pixel;
                    gcv.fill_style = x11::xlib::FillStippled;
                    gcv.stipple = stipple;
                    *color.gc.borrow_mut() = x11::xlib::XCreateGC(
                        scr.display,
                        (*scr.rcontext).drawable,
                        (x11::xlib::GCForeground
                            | x11::xlib::GCBackground
                            | x11::xlib::GCStipple
                            | x11::xlib::GCFillStyle
                            | x11::xlib::GCGraphicsExposures) as u64,
                        &mut gcv,
                    );
                    x11::xlib::XFreePixmap(scr.display, stipple);
                    if let Some(white) = NonNull::new(white) {
                        WMReleaseColor(white);
                    }
                    if let Some(black) = NonNull::new(black) {
                        WMReleaseColor(black);
                    }
                } else {
                    scr.darkGray = WMCreateRGBColor(screen, 0x5144, 0x5555, 0x5144, 1);
                    // TODO: warn or bail if we couldn't allocate.
                }
            }
            NonNull::new(scr.darkGray)
                .map(|c| WMRetainColor(c))
                .unwrap_or(ptr::null_mut())
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMRedComponentOfColor(color: NonNull<WMColor>) -> c_ushort {
        unsafe { (*color.as_ptr()).red() }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMGreenComponentOfColor(color: NonNull<WMColor>) -> c_ushort {
        unsafe { (*color.as_ptr()).green() }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMBlueComponentOfColor(color: NonNull<WMColor>) -> c_ushort {
        unsafe { (*color.as_ptr()).blue() }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMGetColorAlpha(color: NonNull<WMColor>) -> c_ushort {
        unsafe { (*color.as_ptr()).alpha() }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMGetColorRGBDescription(color: NonNull<WMColor>) -> *const c_char {
        unsafe {
            wutil_rs::string::wstrdup(
                CString::new((*color.as_ptr()).hex_triplet())
                    .unwrap()
                    .as_ptr(),
            )
        }
    }
}
