use std::cmp::PartialEq;
use std::ffi::CStr;
use std::fmt;
use std::ptr;

use mupdf_sys::*;

use crate::{context, ColorParams, Error};

#[derive(Debug)]
pub struct Colorspace {
    pub(crate) inner: *mut fz_colorspace,
}

impl Colorspace {
    pub(crate) unsafe fn from_raw(inner: *mut fz_colorspace) -> Self {
        Self { inner }
    }

    pub fn device_gray() -> Self {
        let inner = unsafe { fz_device_gray(context()) };
        Self { inner }
    }

    pub fn device_rgb() -> Self {
        let inner = unsafe { fz_device_rgb(context()) };
        Self { inner }
    }

    pub fn device_bgr() -> Self {
        let inner = unsafe { fz_device_bgr(context()) };
        Self { inner }
    }

    pub fn device_cmyk() -> Self {
        let inner = unsafe { fz_device_cmyk(context()) };
        Self { inner }
    }

    pub fn n(&self) -> u32 {
        unsafe { fz_colorspace_n(context(), self.inner) as u32 }
    }

    pub fn is_gray(&self) -> bool {
        unsafe { fz_colorspace_is_gray(context(), self.inner) > 0 }
    }

    pub fn is_rgb(&self) -> bool {
        unsafe { fz_colorspace_is_rgb(context(), self.inner) > 0 }
    }

    pub fn is_cmyk(&self) -> bool {
        unsafe { fz_colorspace_is_cmyk(context(), self.inner) > 0 }
    }

    pub fn is_device(&self) -> bool {
        unsafe { fz_colorspace_is_device(context(), self.inner) > 0 }
    }

    pub fn is_device_gray(&self) -> bool {
        unsafe { fz_colorspace_is_device_gray(context(), self.inner) > 0 }
    }

    pub fn is_device_cmyk(&self) -> bool {
        unsafe { fz_colorspace_is_device_cmyk(context(), self.inner) > 0 }
    }

    pub fn is_indexed(&self) -> bool {
        unsafe { fz_colorspace_is_indexed(context(), self.inner) > 0 }
    }

    pub fn is_lab(&self) -> bool {
        unsafe { fz_colorspace_is_lab(context(), self.inner) > 0 }
    }

    pub fn is_lab_icc(&self) -> bool {
        unsafe { fz_colorspace_is_lab_icc(context(), self.inner) > 0 }
    }

    pub fn is_subtractive(&self) -> bool {
        unsafe { fz_colorspace_is_subtractive(context(), self.inner) > 0 }
    }

    pub fn name(&self) -> &str {
        let ptr = unsafe { fz_colorspace_name(context(), self.inner) };
        let name_cstr = unsafe { CStr::from_ptr(ptr) };
        name_cstr.to_str().unwrap()
    }

    pub fn convert_color(
        &self,
        color: &[f32],
        to: &Colorspace,
        via: Option<&Colorspace>,
        params: ColorParams,
    ) -> Result<Vec<f32>, Error> {
        let from_n = usize::try_from(self.n()).unwrap();
        assert!(color.len() >= from_n);
        let to_n = usize::try_from(to.n()).unwrap();

        let via = via.map_or(ptr::null_mut(), |cs| cs.inner);
        unsafe {
            let mut out = Vec::with_capacity(to_n);
            ffi_try!(mupdf_convert_color(
                context(),
                self.inner,
                color.as_ptr(),
                to.inner,
                out.as_mut_ptr(),
                via,
                params.into()
            ))?;
            out.set_len(to_n);
            Ok(out)
        }
    }

    pub fn convert_color_into(
        &self,
        color: &[f32],
        to: &Colorspace,
        out: &mut [f32],
        via: Option<&Colorspace>,
        params: ColorParams,
    ) -> Result<usize, Error> {
        let from_n = usize::try_from(self.n()).unwrap();
        assert!(color.len() >= from_n);
        let n = usize::try_from(to.n()).unwrap();
        assert!(out.len() >= n);

        let via = via.map_or(ptr::null_mut(), |cs| cs.inner);
        unsafe {
            ffi_try!(mupdf_convert_color(
                context(),
                self.inner,
                color.as_ptr(),
                to.inner,
                out.as_mut_ptr(),
                via,
                params.into()
            ))?;
            Ok(n)
        }
    }
}

impl PartialEq for Colorspace {
    fn eq(&self, other: &Self) -> bool {
        self.inner == other.inner
    }
}

impl fmt::Display for Colorspace {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let name = self.name();
        if name.starts_with("Device") {
            name.fmt(f)
        } else {
            write!(f, "ColorSpace({})", self.n())
        }
    }
}

#[cfg(test)]
mod test {
    use crate::ColorParams;

    use super::Colorspace;

    #[test]
    fn test_color_space_device_colors() {
        let gray = Colorspace::device_gray();
        assert!(gray.is_device_gray());
        assert!(gray.is_gray());
        assert_eq!(gray.name(), "DeviceGray");

        let rgb = Colorspace::device_rgb();
        assert!(rgb.is_rgb());
        assert_eq!(rgb.name(), "DeviceRGB");

        let bgr = Colorspace::device_bgr();
        assert_eq!(bgr.name(), "DeviceBGR");

        let cmyk = Colorspace::device_cmyk();
        assert!(cmyk.is_device_cmyk());
        assert!(cmyk.is_cmyk());
        assert_eq!(cmyk.name(), "DeviceCMYK");
    }

    #[test]
    fn test_color_conversion() {
        let red = Colorspace::device_rgb()
            .convert_color(
                &[1.0, 0.0, 0.0],
                &Colorspace::device_cmyk(),
                None,
                ColorParams::default(),
            )
            .unwrap();
        assert_eq!(red.len(), 4);
        assert_eq!(red[0], 0.0);
        assert!((0.99..=1.0).contains(&red[1]));
        assert_eq!(red[2], 1.0);
        assert_eq!(red[3], 0.0);

        let mut gray = [0.0; 4];
        let n = Colorspace::device_gray()
            .convert_color_into(
                &[0.6],
                &Colorspace::device_rgb(),
                &mut gray,
                None,
                ColorParams::default(),
            )
            .unwrap();
        assert_eq!(n, 3);
        assert!((0.59..0.61).contains(&gray[0]), "gray = {gray:?}");
        assert!((0.59..0.61).contains(&gray[1]), "gray = {gray:?}");
        assert!((0.59..0.61).contains(&gray[2]), "gray = {gray:?}");
        assert_eq!(gray[3], 0.0);
    }
}
