use crate::{Float, Ptr};
use crate::core::color::{RGB, XYZ};
use enum_dispatch::enum_dispatch;

pub use crate::spectra::*;

#[enum_dispatch]
pub trait SpectrumTrait {
    fn evaluate(&self, lambda: Float) -> Float;
    fn sample(&self, lambda: &SampledWavelengths) -> SampledSpectrum {
        SampledSpectrum::from_fn(|i| self.evaluate(lambda[i]))
    }
    fn max_value(&self) -> Float;
}

#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct StandardSpectra {
    pub x: Ptr<DenselySampledSpectrum>,
    pub y: Ptr<DenselySampledSpectrum>,
    pub z: Ptr<DenselySampledSpectrum>,
    pub d65: Ptr<DenselySampledSpectrum>,
}

unsafe impl Send for StandardSpectra {}
unsafe impl Sync for StandardSpectra {}

#[repr(C)]
#[enum_dispatch(SpectrumTrait)]
#[derive(Debug, Clone, Copy)]
pub enum Spectrum {
    Constant(ConstantSpectrum),
    Dense(Ptr<DenselySampledSpectrum>),
    Piecewise(Ptr<PiecewiseLinearSpectrum>),
    Blackbody(BlackbodySpectrum),
    RGBAlbedo(RGBAlbedoSpectrum),
    RGBIlluminant(RGBIlluminantSpectrum),
    RGBUnbounded(RGBUnboundedSpectrum),
}

impl<T: SpectrumTrait> SpectrumTrait for Ptr<T> {
    fn evaluate(&self, lambda: Float) -> Float {
        unsafe { self.as_ref().evaluate(lambda) }
    }
    fn max_value(&self) -> Float {
        unsafe { self.as_ref().max_value() }
    }
}

impl Spectrum {
    pub fn std_illuminant_d65() -> Self {
        unimplemented!("Use crate::spectra::default_illuminant() on host")
    }

    pub fn to_xyz(&self, std: &StandardSpectra) -> XYZ {
        let x = self.inner_product(&Spectrum::Dense(std.x));
        let y = self.inner_product(&Spectrum::Dense(std.y));
        let z = self.inner_product(&Spectrum::Dense(std.z));

        XYZ::new(x, y, z) / CIE_Y_INTEGRAL
    }

    pub fn to_rgb(&self, cs: &RGBColorSpace, std: &StandardSpectra) -> RGB {
        let xyz = self.to_xyz(std);
        cs.to_rgb(xyz)
    }

    pub fn inner_product(&self, other: &Spectrum) -> Float {
        let mut integral = 0.0;
        for lambda in LAMBDA_MIN..=LAMBDA_MAX {
            let l = lambda as Float;
            integral += self.evaluate(l) * other.evaluate(l);
        }
        integral
    }

    pub fn is_constant(&self) -> bool {
        matches!(self, Spectrum::Constant(_))
    }
}
