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

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

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

#[enum_dispatch(FloatTextureTrait)]
#[derive(Debug, Clone)]
pub enum FloatTexture {
    Constant(FloatConstantTexture),
    Image(FloatImageTexture),
    Mix(FloatMixTexture),
    DirectionMix(FloatDirectionMixTexture),
    Scaled(FloatScaledTexture),
    Bilerp(FloatBilerpTexture),
    Checkerboard(FloatCheckerboardTexture),
    Dots(FloatDotsTexture),
    FBm(FBmTexture),
    Ptex(FloatPtexTexture),
    Windy(WindyTexture),
    Wrinkled(WrinkledTexture),
}

impl FloatTexture {
    pub fn create(
        name: &str,
        render_from_texture: &Transform,
        params: &TextureParameterDictionary,
        loc: &FileLoc,
        arena: &mut Arena,
    ) -> Result<Self, String> {
        match name {
            "constant" => {
                let tex = FloatConstantTexture::create(render_from_texture, params, loc);
                Ok(FloatTexture::Constant(tex))
            }
            "scale" => Ok(FloatScaledTexture::create(
                render_from_texture,
                params,
                loc,
                arena,
            )),
            "mix" => {
                let tex = FloatMixTexture::create(render_from_texture, params, loc, arena);
                Ok(FloatTexture::Mix(tex))
            }
            "directionmix" => {
                let tex = FloatDirectionMixTexture::create(render_from_texture, params, loc, arena);
                Ok(FloatTexture::DirectionMix(tex))
            }
            "bilerp" => {
                let tex = FloatBilerpTexture::create(render_from_texture, params, loc);
                Ok(FloatTexture::Bilerp(tex))
            }
            "imagemap" => {
                let tex = FloatImageTexture::create(render_from_texture, params, loc);
                Ok(FloatTexture::Image(tex))
            }
            "checkerboard" => {
                let tex = FloatCheckerboardTexture::create(render_from_texture, params, loc);
                Ok(FloatTexture::Checkerboard(tex))
            }
            "dots" => {
                let tex = FloatDotsTexture::create(render_from_texture, params, loc);
                Ok(FloatTexture::Dots(tex))
            }
            "fbm" => {
                let tex = FBmTexture::create(render_from_texture, params, loc);
                Ok(FloatTexture::FBm(tex))
            }
            "wrinkled" => {
                let tex = WrinkledTexture::create(render_from_texture, params, loc);
                Ok(FloatTexture::Wrinkled(tex))
            }
            "windy" => {
                let tex = WindyTexture::create(render_from_texture, params, loc);
                Ok(FloatTexture::Windy(tex))
            }
            "ptex" => {
                let tex = FloatPtexTexture::create(render_from_texture, params, loc);
                Ok(FloatTexture::Ptex(tex))
            }
            _ => Err(format!("Float texture type '{}' unknown at {}", name, loc)),
        }
    }
}

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

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

impl CreateTextureMapping for TextureMapping2D {
    fn create(
        params: &TextureParameterDictionary,
        render_from_texture: &Transform,
        loc: &FileLoc,
    ) -> Self {
        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);
                TextureMapping2D::UV(mapping)
            }
            "spherical" => {
                let mapping = SphericalMapping::new(&render_from_texture.inverse());
                TextureMapping2D::Spherical(mapping)
            }
            "cylindrical" => {
                let mapping = CylindricalMapping::new(&render_from_texture.inverse());
                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);
                TextureMapping2D::Planar(mapping)
            }
            _ => {
                log::error!("{}: 2D texture mapping unknown {}", loc, mtype);
                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,
}
