use std::path::Path;

use miette::IntoDiagnostic;

use super::*;

#[derive(Debug)]
pub(super) enum ImageMapWrap {
    Repeat,
    Black,
    Clamp,
}

impl ImageMapWrap {
    pub(super) fn new(x: String) -> Result<Self> {
        match x.as_str() {
            "repeat" => Ok(Self::Repeat),
            "black" => Ok(Self::Black),
            "clamp" => Ok(Self::Clamp),
            _ => Err(miette!("error image map wrap")),
        }
    }
}

#[derive(Debug)]
pub(super) enum ImageMapEncoding {
    #[allow(clippy::upper_case_acronyms)]
    SRGB,
    Linear,
    Gamma(Float),
}

impl ImageMapEncoding {
    pub(super) fn new(x: String) -> Result<Self> {
        match x.as_str() {
            "sRGB" => Ok(Self::SRGB),
            "linear" => Ok(Self::Linear),
            s if s.starts_with("gamma ") => {
                Ok(Self::Gamma(s.split_at(7).1.parse().into_diagnostic()?))
            }
            _ => Err(miette!("error image map encoding")),
        }
    }
}

pub(super) struct SpectrumImageMapTexture {
    mapping: UVMapping,
    scale: Float,
    wrap: ImageMapWrap,
    invert: bool,
    image: image::Rgb32FImage,
}

impl std::fmt::Debug for SpectrumImageMapTexture {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("SpectrumImageMapTexture")
            .field("mapping", &self.mapping)
            .field("scale", &self.scale)
            .field("wrap", &self.wrap)
            .field("invert", &self.invert)
            .field("image", &"image data")
            .finish()
    }
}

fn srgb_nonlinear(x: f32) -> f32 {
    if x <= 0.04045 {
        x / 12.92
    } else {
        ((x + 0.055) / 1.055).powf(2.4)
    }
}

impl SpectrumImageMapTexture {
    pub(super) fn new(
        mapping: UVMapping,
        scale: Float,
        wrap: ImageMapWrap,
        encoding: ImageMapEncoding,
        invert: bool,
        path: impl AsRef<Path>,
    ) -> Result<Self> {
        let image = image::open(path).unwrap();

        let mut image = image.into_rgb32f();

        match encoding {
            ImageMapEncoding::SRGB => {
                for pixel in image.pixels_mut() {
                    pixel.0 = [
                        srgb_nonlinear(pixel.0[0]),
                        srgb_nonlinear(pixel.0[1]),
                        srgb_nonlinear(pixel.0[2]),
                    ];
                }
            }
            ImageMapEncoding::Linear => (),
            ImageMapEncoding::Gamma(gamma) => {
                for pixel in image.pixels_mut() {
                    pixel.0 = [
                        pixel.0[0].powf(gamma),
                        pixel.0[1].powf(gamma),
                        pixel.0[2].powf(gamma),
                    ];
                }
            }
        }
        Ok(Self {
            mapping,
            scale,
            invert,
            wrap,
            image,
        })
    }
}

impl Pbrt2dSpectrumTexture for SpectrumImageMapTexture {
    fn get(&self, u: Float, v: Float) -> Color {
        let (u, v) = self.mapping.map(u, v);

        let (w, h) = self.image.dimensions();

        let (u, v) = match self.wrap {
            ImageMapWrap::Repeat => (
                if u < 0.0 { 1.0 + u.fract() } else { u.fract() },
                if v < 0.0 { 1.0 + v.fract() } else { v.fract() },
            ),
            ImageMapWrap::Black => {
                if !(0.0..=1.0).contains(&u) || !(0.0..=1.0).contains(&v) {
                    return Color::black();
                }

                (u, v)
            }
            ImageMapWrap::Clamp => (u.clamp(0.0, 1.0), v.clamp(0.0, 1.0)),
        };

        let x = u * w as Float;
        let y = v * h as Float;

        let c = self
            .image
            .get_pixel((x as u32).clamp(0, w - 1), (y as u32).clamp(0, h - 1));

        Color::new(c.0[0], c.0[1], c.0[2])
    }
}

impl PbrtTexture for SpectrumImageMapTexture {
    fn get_2d_float_texture(self: Arc<Self>) -> Result<Arc<dyn Pbrt2dFloatTexture>> {
        Err(miette!("Unable to use this texture as a float texture"))
    }

    fn get_2d_spectrum_texture(self: Arc<Self>) -> Result<Arc<dyn Pbrt2dSpectrumTexture>> {
        Ok(self)
    }

    fn get_dimension(&self) -> TextureDimension {
        TextureDimension::D2
    }
}
