use crate::WINGsP;
use std::{
    cell::RefCell,
    ffi::CStr,
    ptr::NonNull,
};

pub struct Pixmap {
    display: NonNull<x11::xlib::Display>,
    clip_gc: x11::xlib::GC,
    pixmap: RefCell<x11::xlib::Pixmap>,
    mask: RefCell<x11::xlib::Pixmap>,
    width: u16,
    height: u16,
    depth: u16,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Masked {
    Yes,
    No,
}

impl Pixmap {
    pub fn new(screen: NonNull<WINGsP::WMScreen>, width: u16, height: u16, depth: u16, masked: Masked) -> Self {
        let display = NonNull::new(unsafe { (*screen.as_ptr()).display }).unwrap();
        let drawable = unsafe { (*(*screen.as_ptr()).rcontext).drawable };
        let clip_gc = unsafe { (*screen.as_ptr()).clipGC };
        let pixmap = unsafe { x11::xlib::XCreatePixmap(display.as_ptr(), drawable, width.into(), height.into(), depth.into()) };
        let mask = match masked {
            Masked::Yes => RefCell::new(unsafe { x11::xlib::XCreatePixmap(display.as_ptr(), drawable, width.into(), height.into(), 1) }),
            Masked::No => RefCell::new(0),
        };
        Pixmap {
            display,
            clip_gc,
            pixmap: RefCell::new(pixmap),
            mask,
            width,
            height,
            depth,
        }
    }

    pub fn new_from_x_pixmaps(
        screen: NonNull<WINGsP::WMScreen>,
        pixmap: x11::xlib::Pixmap,
        mask: x11::xlib::Pixmap,
        width: u16,
        height: u16,
        depth: u16,
    ) -> Self {
        let display = NonNull::new(unsafe { (*screen.as_ptr()).display }).unwrap();
        let clip_gc = unsafe { (*screen.as_ptr()).clipGC };
        Pixmap {
            display,
            clip_gc,
            pixmap: RefCell::new(pixmap),
            mask: RefCell::new(mask),
            width,
            height,
            depth,
        }
    }

    pub fn new_from_file(
        screen: NonNull<WINGsP::WMScreen>,
        file_name: &CStr,
    ) -> Option<Self> {
        let rcontext = NonNull::new(unsafe { (*screen.as_ptr()).rcontext }).unwrap();
        let Some(image) = NonNull::new(unsafe { wrlib_rs::ffi::RLoadImage(rcontext.as_ptr(), file_name.as_ptr(), 0) }) else {
            return None;
        };
        let result = Self::new_from_r_image(screen, image, 127);
        unsafe { wrlib_rs::ffi::RReleaseImage(image.as_ptr()) };
        result
    }

    pub fn new_from_r_image(
        screen: NonNull<WINGsP::WMScreen>,
        image: NonNull<wrlib_rs::ffi::RImage>,
        threshold: u16,
    ) -> Option<Self> {
        let display = NonNull::new(unsafe { (*screen.as_ptr()).display }).unwrap();
        let clip_gc = unsafe { (*screen.as_ptr()).clipGC };
        let depth: u16 = unsafe { (*screen.as_ptr()).depth.try_into().expect("invalid depth") };
        let rcontext = NonNull::new(unsafe { (*screen.as_ptr()).rcontext }).unwrap();
        let mut pixmap: x11::xlib::Pixmap = 0;
        let mut mask: x11::xlib::Pixmap = 0;
        if unsafe { wrlib_rs::ffi::RConvertImageMask(rcontext.as_ptr(), image.as_ptr(), &mut pixmap, &mut mask, threshold.into()) } == 0 {
            return None;
        }

        let (width, height) = unsafe {
            ((*image.as_ptr()).width.try_into().expect("invalid width"),
             (*image.as_ptr()).height.try_into().expect("invalid height"))
        };
        Some(Pixmap {
            display,
            clip_gc,
            pixmap: RefCell::new(pixmap),
            mask: RefCell::new(mask),
            width,
            height,
            depth,
        })
    }

    pub fn draw(&self, drawable: x11::xlib::Drawable, x: i32, y: i32) {
        unsafe {
            x11::xlib::XSetClipMask(self.display.as_ptr(), self.clip_gc, *self.mask.borrow());
            x11::xlib::XSetClipOrigin(self.display.as_ptr(), self.clip_gc, x, y);
            x11::xlib::XCopyArea(
                self.display.as_ptr(),
                *self.pixmap.borrow(),
                drawable,
                self.clip_gc,
                0,
                0,
                self.width.into(),
                self.height.into(),
                x,
                y,
            );
        }
    }

    pub fn pixmap(&self) -> x11::xlib::Pixmap {
        *self.pixmap.borrow()
    }

    pub fn mask(&self) -> x11::xlib::Pixmap {
        *self.mask.borrow()
    }
}

/// Attempts to load an `RImage` from `file_name` and returns the value of invoking `f` on it.
///
/// If image loading fails, `f` is invoked on a `None` value.
///
/// For example:
///
/// ```ignore
/// # use std::ffi::CString;
/// # use wings_rs::pixmap::with_r_image_from_file;
/// # fn main() {
/// #     let rcontext = unsafe { wrlib_rs::RCreateContext(display, 0, ptr::null_mut()) };
/// let (width, height) = with_r_image_from_file(
///     rcontext,
///     &CString::new("test_image_32x48.png").unwrap(),
///     |image| {
///         let image = image.expect("image loading failed");
///         ((*image.as_ptr()).width, (*image.as_ptr()).height)
///     }
/// );
/// assert_eq!(width, 32);
/// assert_eq!(height, 48);
/// #     unsafe { wrlib_rs::RDestroyContext(rcontext); }
/// # }
/// ```
pub fn with_r_image_from_file<R>(
    rcontext: NonNull<wrlib_rs::ffi::RContext>,
    file_name: &CStr,
    f: impl FnOnce(Option<NonNull<wrlib_rs::ffi::RImage>>) -> R,
) -> R {
    let image = NonNull::new(unsafe {
        wrlib_rs::ffi::RLoadImage(rcontext.as_ptr(), file_name.as_ptr(), 0)
    });
    let result = f(image);
    if let Some(image) = image {
        unsafe { wrlib_rs::ffi::RReleaseImage(image.as_ptr()); }
    }
    result
}

#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub enum Transform {
    Downscale { width: u32, height: u32 },
    Greymask,
}

impl Transform {
    pub fn with_transformed<R>(
        mut image: NonNull<wrlib_rs::ffi::RImage>,
        transforms: &[Transform],
        f: impl FnOnce(NonNull<wrlib_rs::ffi::RImage>) -> R,
    ) -> R {
        let mut transforms_i = transforms.iter();
        if let Some(t) = transforms_i.next() {
            image = t.apply(image);
        }
        for t in transforms_i {
            let next_image = t.apply(image);
            unsafe { wrlib_rs::ffi::RReleaseImage(image.as_ptr()); }
            image = next_image;
        }
        let result = f(image);
        if !transforms.is_empty() {
            unsafe { wrlib_rs::ffi::RReleaseImage(image.as_ptr()); }
        }
        result
    }

    fn apply(
        &self,
        image: NonNull<wrlib_rs::ffi::RImage>,
    ) -> NonNull<wrlib_rs::ffi::RImage> {
        match self {
            Transform::Downscale { width, height } => {
                let Ok(current_width): Result<u32, _> = (unsafe { (*image.as_ptr()).width.try_into() }) else {
                    return image;
                };
                let Ok(current_height): Result<u32, _> = (unsafe { (*image.as_ptr()).height.try_into() }) else {
                    return image;
                };
                if current_width <= *width && current_height <= *height {
                    return image;
                }

                let mut new_width = current_width;
                let mut new_height = current_height;
                if new_width > *width {
                    new_width = *width;
                    new_height = *width * current_height / current_width;
                }
                if new_height > *height {
                    new_width = *height * current_width / current_height;
                    new_height = *height;
                }
                let image = unsafe { wrlib_rs::ffi::RScaleImage(image.as_ptr(), new_width, new_height) };
                NonNull::new(image).expect("RScaleImage failed")
            }
            Transform::Greymask => {
                const GREY: wrlib_rs::ffi::RColor = wrlib_rs::ffi::RColor {
                    red: 0xAE,
                    green: 0xAA,
                    blue: 0xAE,
                    alpha: 0xFF,
                };
                unsafe {
                    let result = NonNull::new(wrlib_rs::ffi::RCloneImage(image.as_ptr()))
                        .expect("RCloneImage failed");
                    wrlib_rs::ffi::RCombineImageWithColor(result.as_ptr(), &GREY as *const _);
                    result
                }
            }
        }
    }
}

impl Drop for Pixmap {
    fn drop(&mut self) {
        unsafe {
            if *self.pixmap.borrow() != 0 {
                x11::xlib::XFreePixmap(self.display.as_ptr(), *self.pixmap.borrow());
            }
            if *self.mask.borrow() != 0 {
                x11::xlib::XFreePixmap(self.display.as_ptr(), *self.mask.borrow());
            }
        }
    }
}

pub mod ffi {
    use super::*;
    use std::{
        ffi::{c_char, c_int, c_uint, c_ushort},
        ptr,
        rc::Rc,
    };

    pub type WMPixmap = Rc<Pixmap>;

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMRetainPixmap(pixmap: *mut WMPixmap) -> *mut WMPixmap {
        if pixmap.is_null() {
            return ptr::null_mut();
        }
        Box::leak(Box::new(unsafe { (*pixmap).clone() })) as *mut _
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMReleasePixmap(pixmap: *mut WMPixmap) {
        let _ = unsafe { Box::from_raw(pixmap) };
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMCreatePixmap(
        screen: *mut WINGsP::WMScreen,
        width: c_ushort,
        height: c_ushort,
        depth: c_ushort,
        masked: c_int,
    ) -> *mut WMPixmap {
        let pixmap = Pixmap::new(
            NonNull::new(screen).expect("pixmap screen cannot be null"),
            width,
            height,
            depth,
            if masked == 0 {
                Masked::No
            } else {
                Masked::Yes
            },
        );
        Box::leak(Box::new(Rc::new(pixmap))) as *mut _
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMCreatePixmapFromXPixmaps(
        screen: *mut WINGsP::WMScreen,
        pixmap: x11::xlib::Pixmap,
        mask: x11::xlib::Pixmap,
        width: c_ushort,
        height: c_ushort,
        depth: c_ushort,
    ) -> *mut WMPixmap {
        let Some(screen) = NonNull::new(screen) else {
            return ptr::null_mut();
        };
        let pixmap = Pixmap::new_from_x_pixmaps(
            screen,
            pixmap,
            mask,
            width,
            height,
            depth,
        );
        Box::leak(Box::new(Rc::new(pixmap))) as *mut _
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMCreatePixmapFromFile(
        screen: *mut WINGsP::WMScreen,
        file_name: *const c_char,
    ) -> *mut WMPixmap {
        let Some(screen) = NonNull::new(screen) else {
            return ptr::null_mut();
        };
        if file_name.is_null() {
            return ptr::null_mut();
        }
        let file_name = unsafe { CStr::from_ptr(file_name) };
        let Some(pixmap) = Pixmap::new_from_file(screen, file_name) else {
            return ptr::null_mut();
        };
        Box::leak(Box::new(Rc::new(pixmap))) as *mut _
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMCreatePixmapFromRImage(
        screen: *mut WINGsP::WMScreen,
        image: *mut wrlib_rs::ffi::RImage,
        threshold: c_ushort,
    ) -> *mut WMPixmap {
        let Some(screen) = NonNull::new(screen) else {
            return ptr::null_mut();
        };
        let Some(image) = NonNull::new(image) else {
            return ptr::null_mut();
        };
        let Some(pixmap) = Pixmap::new_from_r_image(
            screen,
            image,
            threshold,
        ) else {
            return ptr::null_mut();
        };
        Box::leak(Box::new(Rc::new(pixmap))) as *mut _
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMCreateGreyedPixmapFromRImage(
        screen: *mut WINGsP::WMScreen,
        image: *mut wrlib_rs::ffi::RImage,
    ) -> *mut WMPixmap {
        let Some(screen) = NonNull::new(screen) else {
            return ptr::null_mut();
        };
        let image = NonNull::new(image).expect("image cannot be null");
        let Some(result) = Transform::with_transformed(
            image,
            &[Transform::Greymask],
            |image| Pixmap::new_from_r_image(screen, image, 0)) else {
            return ptr::null_mut();
        };
        Box::leak(Box::new(Rc::new(result))) as *mut _
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMCreateScaledPixmapFromFile(
        screen: *mut WINGsP::WMScreen,
        file_name: *const c_char,
        width: c_uint,
        height: c_uint,
    ) -> *mut WMPixmap {
        let Some(screen) = NonNull::new(screen) else {
            return ptr::null_mut();
        };
        if file_name.is_null() {
            return ptr::null_mut();
        }
        let file_name = unsafe { CStr::from_ptr(file_name) };
        let Some(rcontext) = NonNull::new(unsafe { (*screen.as_ptr()).rcontext }) else {
            return ptr::null_mut();
        };
        let Some(result) =
            with_r_image_from_file(
                rcontext,
                file_name,
                |image| {
                    let Some(image) = image else {
                        return None;
                    };
                    Transform::with_transformed(
                        image,
                        &[Transform::Downscale { width, height }],
                        |image| Pixmap::new_from_r_image(screen, image, 0),
                    )
                }
            ) else {
                return ptr::null_mut();
            };
        Box::leak(Box::new(Rc::new(result))) as *mut _
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMCreateGreyedPixmapFromFile(
        screen: *mut WINGsP::WMScreen,
        file_name: *const c_char,
    ) -> *mut WMPixmap {
        let Some(screen) = NonNull::new(screen) else {
            return ptr::null_mut();
        };
        if file_name.is_null() {
            return ptr::null_mut();
        }
        let file_name = unsafe { CStr::from_ptr(file_name) };
        let Some(rcontext) = NonNull::new(unsafe { (*screen.as_ptr()).rcontext }) else {
            return ptr::null_mut();
        };
        let Some(result) =
            with_r_image_from_file(
                rcontext,
                file_name,
                |image| {
                    let Some(image) = image else {
                        return None;
                    };
                    Transform::with_transformed(
                        image,
                        &[Transform::Greymask],
                        |image| Pixmap::new_from_r_image(screen, image, 0),
                    )
                }
            ) else {
                return ptr::null_mut();
            };
        Box::leak(Box::new(Rc::new(result))) as *mut _
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMCreateScaledGreyedPixmapFromFile(
        screen: *mut WINGsP::WMScreen,
        file_name: *const c_char,
        width: c_uint,
        height: c_uint,
    ) -> *mut WMPixmap {
        let Some(screen) = NonNull::new(screen) else {
            return ptr::null_mut();
        };
        if file_name.is_null() {
            return ptr::null_mut();
        }
        let file_name = unsafe { CStr::from_ptr(file_name) };
        let Some(rcontext) = NonNull::new(unsafe { (*screen.as_ptr()).rcontext }) else {
            return ptr::null_mut();
        };
        let Some(result) = with_r_image_from_file(
            rcontext,
            file_name,
            |image| {
                let Some(image) = image else {
                    return None;
                };
                Transform::with_transformed(
                    image,
                    &[Transform::Downscale { width, height },
                      Transform::Greymask],
                    |image| Pixmap::new_from_r_image(screen, image, 0),
                )
            },
        ) else {
            return ptr::null_mut();
        };
        Box::leak(Box::new(Rc::new(result))) as *mut _
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMCreatePixmapFromXPMData(
        screen: *mut WINGsP::WMScreen,
        data: *mut *mut c_char,
    ) -> *mut WMPixmap {
        todo!()
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMGetPixmapXID(pixmap: *mut WMPixmap) -> x11::xlib::Pixmap {
        let pixmap = NonNull::new(pixmap).expect("pixmap cannot be null");
        
        unsafe { *(&(*pixmap.as_ptr())).pixmap.borrow() }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMSetPixmapXID(pixmap: *mut WMPixmap, value: x11::xlib::Pixmap) {
        let pixmap = NonNull::new(pixmap).expect("pixmap cannot be null");
        
        unsafe { *(&mut (*pixmap.as_ptr())).pixmap.borrow_mut() = value; }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMGetPixmapMaskXID(pixmap: *mut WMPixmap) -> x11::xlib::Pixmap {
        let pixmap = NonNull::new(pixmap).expect("pixmap cannot be null");

        unsafe { *(&(*pixmap.as_ptr())).mask.borrow() }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMSetPixmapMaskXID(pixmap: *mut WMPixmap, mask: x11::xlib::Pixmap) {
        let pixmap = NonNull::new(pixmap).expect("pixmap cannot be null");

        unsafe { *(&mut (*pixmap.as_ptr())).mask.borrow_mut() = mask; }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMGetPixmapSize(pixmap: *mut WMPixmap) -> WINGsP::WMSize {
        let pixmap = NonNull::new(pixmap).expect("pixmap cannot be null");

        let (width, height) = unsafe {
            ((&(*pixmap.as_ptr())).width, (&(*pixmap.as_ptr())).height)
        };
        WINGsP::WMSize {
            width: width.into(),
            height: height.into(),
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMGetPixmapDepth(pixmap: *mut WMPixmap) -> c_int {
        let pixmap = NonNull::new(pixmap).expect("pixmap cannot be null");

        unsafe { (&(*pixmap.as_ptr())).depth.into() }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMGetSystemPixmap(screen: *mut WINGsP::WMScreen, image: c_uint) -> *mut WMPixmap {
        unsafe {
            let screen = &*screen;
            match image {
                WINGsP::constants::WSystemIcon_ReturnArrow => WMRetainPixmap(screen.buttonArrow),
                WINGsP::constants::WSystemIcon_HighlightedReturnArrow => WMRetainPixmap(screen.pushedButtonArrow),
	        WINGsP::constants::WSystemIcon_ScrollerDimple => WMRetainPixmap(screen.scrollerDimple),
	        WINGsP::constants::WSystemIcon_ArrowLeft => WMRetainPixmap(screen.leftArrow),
	        WINGsP::constants::WSystemIcon_HighlightedArrowLeft => WMRetainPixmap(screen.hiLeftArrow),
	        WINGsP::constants::WSystemIcon_ArrowRight => WMRetainPixmap(screen.rightArrow),
	        WINGsP::constants::WSystemIcon_HighlightedArrowRight => WMRetainPixmap(screen.hiRightArrow),
	        WINGsP::constants::WSystemIcon_ArrowUp => WMRetainPixmap(screen.upArrow),
	        WINGsP::constants::WSystemIcon_HighlightedArrowUp => WMRetainPixmap(screen.hiUpArrow),
	        WINGsP::constants::WSystemIcon_ArrowDown => WMRetainPixmap(screen.downArrow),
	        WINGsP::constants::WSystemIcon_HighlightedArrowDown => WMRetainPixmap(screen.hiDownArrow),
	        WINGsP::constants::WSystemIcon_CheckMark => WMRetainPixmap(screen.checkMark),
                _ => ptr::null_mut(),
            }
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMDrawPixmap(
        pixmap: *mut WMPixmap,
        drawable: x11::xlib::Drawable,
        x: c_int,
        y: c_int,
    ) {
        unsafe {
            let pixmap = &*pixmap;
            x11::xlib::XSetClipMask(pixmap.display.as_ptr(), pixmap.clip_gc, *pixmap.mask.borrow());
            x11::xlib::XSetClipOrigin(pixmap.display.as_ptr(), pixmap.clip_gc, x, y);
            x11::xlib::XCopyArea(
                pixmap.display.as_ptr(),
                *pixmap.pixmap.borrow(),
                drawable,
                pixmap.clip_gc,
                0,
                0,
                pixmap.width.into(),
                pixmap.height.into(),
                x,
                y,
            );
        }
    }
}
