use crate::textures::*;
use crate::utils::TextureParameterDictionary;
use crate::{Arena, FileLoc};
use anyhow::{anyhow, Result};
use enum_dispatch::enum_dispatch;
use shared::core::color::ColorEncoding;
use shared::core::geometry::Vector3f;
use shared::core::image::WrapMode;
use shared::core::texture::SpectrumType;
use shared::core::texture::{
    CylindricalMapping, GPUFloatTexture, GPUSpectrumTexture, PlanarMapping, SphericalMapping,
    TextureEvalContext, TextureMapping2D, UVMapping,
};
use shared::spectra::{SampledSpectrum, SampledWavelengths};
use shared::textures::*;
use shared::utils::Transform;
use shared::Float;
use std::collections::HashMap;
use std::sync::{Arc, Mutex, OnceLock};

#[enum_dispatch]
pub trait FloatTextureTrait {
    fn evaluate(&self, ctx: &TextureEvalContext) -> Float;
}

#[enum_dispatch]
pub trait SpectrumTextureTrait {
    fn evaluate(&self, _ctx: &TextureEvalContext, _lambda: &SampledWavelengths) -> SampledSpectrum;
}

#[derive(Clone, Debug)]
#[enum_dispatch(FloatTextureTrait)]
pub enum FloatTexture {
    Constant(FloatConstantTexture),
    Checkerboard(FloatCheckerboardTexture),
    Dots(FloatDotsTexture),
    FBm(FBmTexture),
    Windy(WindyTexture),
    Wrinkled(WrinkledTexture),
    Mix(FloatMixTexture),
    DirectionMix(FloatDirectionMixTexture),
    // #[device(custom = "upload_image", variant_type = "GPUFloatImageTexture")]
    Scaled(FloatScaledTexture),
    Image(FloatImageTexture),
    Bilerp(FloatBilerpTexture),
}

impl FloatTexture {
    fn upload_image(inner: &FloatImageTexture, _arena: &Arena) -> GPUFloatImageTexture {
        GPUFloatImageTexture {
            mapping: inner.base.mapping,
            tex_obj: inner.base.mipmap.texture_object(),
            scale: inner.base.scale,
            invert: inner.base.invert,
        }
    }
}

impl Default for FloatTexture {
    fn default() -> Self {
        FloatTexture::Constant(FloatConstantTexture::new(1.0))
    }
}

impl FloatTextureTrait for Arc<FloatTexture> {
    fn evaluate(&self, ctx: &TextureEvalContext) -> Float {
        self.as_ref().evaluate(ctx)
    }
}

pub trait CreateFloatTexture {
    fn create(
        render_from_texture: Transform,
        params: TextureParameterDictionary,
        loc: FileLoc,
        arena: &Arena,
    ) -> Result<FloatTexture>;
}

impl FloatTexture {
    pub fn create(
        name: &str,
        render_from_texture: Transform,
        params: TextureParameterDictionary,
        loc: FileLoc,
        arena: &Arena,
    ) -> Result<Self> {
        match name {
            "constant" => FloatConstantTexture::create(render_from_texture, params, loc, arena),
            "scale" => FloatScaledTexture::create(&render_from_texture, &params, &loc, arena),
            "mix" => FloatMixTexture::create(&render_from_texture, &params, &loc, arena),
            "directionmix" => {
                FloatDirectionMixTexture::create(&render_from_texture, &params, &loc, arena)
            }
            "bilerp" => FloatBilerpTexture::create(render_from_texture, params, loc, arena),
            "imagemap" => FloatImageTexture::create(render_from_texture, params, loc, arena),
            "checkerboard" => {
                FloatCheckerboardTexture::create(render_from_texture, params, loc, arena)
            }
            "dots" => FloatDotsTexture::create(render_from_texture, params, loc, arena),
            "fbm" => FBmTexture::create(render_from_texture, params, loc, arena),
            "wrinkled" => WrinkledTexture::create(render_from_texture, params, loc, arena),
            "windy" => WindyTexture::create(render_from_texture, params, loc, arena),
            _ => Err(anyhow!("Float texture type '{}' unknown at {}", name, loc)),
        }
    }
}

#[derive(Clone, Debug)]
#[enum_dispatch(SpectrumTextureTrait)]
pub enum SpectrumTexture {
    Constant(SpectrumConstantTexture),
    Checkerboard(SpectrumCheckerboardTexture),
    Dots(SpectrumDotsTexture),
    Image(SpectrumImageTexture),
    Bilerp(SpectrumBilerpTexture),
    Scaled(SpectrumScaledTexture),
    Marble(MarbleTexture),
    Mix(SpectrumMixTexture),
    DirectionMix(SpectrumDirectionMixTexture),
}

impl SpectrumTexture {
    fn upload_spectrum_image(
        inner: &SpectrumImageTexture,
        arena: &Arena,
    ) -> GPUSpectrumImageTexture {
        let tex_obj = arena.get_texture_object(&inner.base.mipmap);
        GPUSpectrumImageTexture {
            mapping: inner.base.mapping,
            tex_obj,
            scale: inner.base.scale,
            invert: inner.base.invert,
            is_single_channel: inner.base.mipmap.is_single_channel(),
            color_space: arena.alloc(
                inner.base.mipmap.color_space
                    .clone()
                    .unwrap_or_else(crate::spectra::default_colorspace),
            ),
            spectrum_type: inner.spectrum_type,
        }
    }
}

pub trait CreateSpectrumTexture {
    fn create(
        render_from_texture: Transform,
        parameters: TextureParameterDictionary,
        spectrum_type: SpectrumType,
        loc: FileLoc,
    ) -> Result<SpectrumTexture>;
}

impl SpectrumTexture {
    pub fn create(
        name: &str,
        render_from_texture: Transform,
        params: TextureParameterDictionary,
        spectrum_type: SpectrumType,
        loc: FileLoc,
        _arena: &Arena,
    ) -> Result<Self> {
        match name {
            "constant" => {
                SpectrumConstantTexture::create(render_from_texture, params, spectrum_type, loc)
            }
            "scale" => {
                SpectrumScaledTexture::create(render_from_texture, params, spectrum_type, loc)
            }
            "mix" => SpectrumMixTexture::create(render_from_texture, params, spectrum_type, loc),
            "directionmix" => {
                SpectrumDirectionMixTexture::create(render_from_texture, params, spectrum_type, loc)
            }
            "bilerp" => {
                SpectrumBilerpTexture::create(render_from_texture, params, spectrum_type, loc)
            }
            "imagemap" => {
                SpectrumImageTexture::create(render_from_texture, params, spectrum_type, loc)
            }
            "checkerboard" => {
                SpectrumCheckerboardTexture::create(render_from_texture, params, spectrum_type, loc)
            }
            "dots" => SpectrumDotsTexture::create(render_from_texture, params, spectrum_type, loc),
            _ => Err(anyhow!(
                "Spectrum texture type '{}' unknown at {}",
                name,
                loc
            )),
        }
    }
}

impl SpectrumTextureTrait for Arc<SpectrumTexture> {
    fn evaluate(&self, ctx: &TextureEvalContext, lambda: &SampledWavelengths) -> SampledSpectrum {
        self.as_ref().evaluate(ctx, lambda)
    }
}

pub trait CreateTextureMapping {
    fn create(
        params: &TextureParameterDictionary,
        render_from_texture: &Transform,
        loc: &FileLoc,
    ) -> Result<Self>
    where
        Self: Sized;
}

impl CreateTextureMapping for TextureMapping2D {
    fn create(
        params: &TextureParameterDictionary,
        render_from_texture: &Transform,
        loc: &FileLoc,
    ) -> Result<Self>
    where
        Self: Sized,
    {
        let mtype = params.get_one_string("mapping", "uv")?;
        match mtype.as_str() {
            "uv" => {
                let su = params.get_one_float("uscale", 1.)?;
                let sv = params.get_one_float("vscale", 1.)?;
                let du = params.get_one_float("udelta", 0.)?;
                let dv = params.get_one_float("vdelta", 0.)?;
                let mapping = UVMapping::new(su, sv, du, dv);
                Ok(TextureMapping2D::UV(mapping))
            }
            "spherical" => {
                let mapping = SphericalMapping::new(&render_from_texture.inverse());
                Ok(TextureMapping2D::Spherical(mapping))
            }
            "cylindrical" => {
                let mapping = CylindricalMapping::new(&render_from_texture.inverse());
                Ok(TextureMapping2D::Cylindrical(mapping))
            }
            "planar" => {
                let vs = params.get_one_vector3f("v1", Vector3f::new(1., 0., 0.))?;
                let vt = params.get_one_vector3f("v2", Vector3f::new(0., 1., 0.))?;
                let ds = params.get_one_float("udelta", 0.)?;
                let dt = params.get_one_float("vdelta", 0.)?;
                let mapping = PlanarMapping::new(&render_from_texture.inverse(), vs, vt, ds, dt);
                Ok(TextureMapping2D::Planar(mapping))
            }
            _ => {
                log::error!("{}: 2D texture mapping unknown {}", loc, mtype);
                Ok(TextureMapping2D::UV(UVMapping::default()))
            }
        }
    }
}

pub static TEXTURE_CACHE: OnceLock<Mutex<HashMap<TexInfo, Arc<MIPMap>>>> = OnceLock::new();

pub fn get_texture_cache() -> &'static Mutex<HashMap<TexInfo, Arc<MIPMap>>> {
    TEXTURE_CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}

#[derive(Debug, Hash, PartialEq, Eq, Clone)]
pub struct TexInfo {
    pub filename: String,
    pub filter_options: MIPMapFilterOptions,
    pub wrap_mode: WrapMode,
    pub encoding: ColorEncoding,
}

