use crate::bxdfs::{
    CoatedConductorBxDF, CoatedDiffuseBxDF, ConductorBxDF, DielectricBxDF, DiffuseBxDF,
};
use crate::core::bsdf::BSDF;
use crate::core::bssrdf::BSSRDF;
use crate::core::bxdf::BxDF;
use crate::core::image::Image;
use crate::core::material::{Material, MaterialEvalContext, MaterialTrait};
use crate::core::scattering::TrowbridgeReitzDistribution;
use crate::core::spectrum::{Spectrum, SpectrumTrait};
use crate::core::texture::{GPUFloatTexture, GPUSpectrumTexture, TextureEvaluator};
use crate::spectra::{SampledSpectrum, SampledWavelengths};
use crate::utils::Ptr;
use crate::utils::math::clamp;

#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct CoatedDiffuseMaterial {
    pub normal_map: Ptr<Image>,
    pub displacement: Ptr<GPUFloatTexture>,
    pub reflectance: Ptr<GPUSpectrumTexture>,
    pub albedo: Ptr<GPUSpectrumTexture>,
    pub u_roughness: Ptr<GPUFloatTexture>,
    pub v_roughness: Ptr<GPUFloatTexture>,
    pub thickness: Ptr<GPUFloatTexture>,
    pub g: Ptr<GPUFloatTexture>,
    pub eta: Ptr<Spectrum>,
    pub max_depth: u32,
    pub n_samples: u32,
    pub remap_roughness: bool,
    pub seed: i32,
}

impl CoatedDiffuseMaterial {
    #[allow(clippy::too_many_arguments)]
    pub fn new(
        reflectance: Ptr<GPUSpectrumTexture>,
        u_roughness: Ptr<GPUFloatTexture>,
        v_roughness: Ptr<GPUFloatTexture>,
        thickness: Ptr<GPUFloatTexture>,
        albedo: Ptr<GPUSpectrumTexture>,
        g: Ptr<GPUFloatTexture>,
        displacement: Ptr<GPUFloatTexture>,
        eta: Ptr<Spectrum>,
        normal_map: Ptr<Image>,
        remap_roughness: bool,
        max_depth: u32,
        n_samples: u32,
        seed: i32,
    ) -> Self {
        Self {
            displacement,
            normal_map,
            reflectance,
            albedo,
            u_roughness,
            v_roughness,
            thickness,
            g,
            eta,
            remap_roughness,
            max_depth,
            n_samples,
            seed,
        }
    }
}

impl MaterialTrait for CoatedDiffuseMaterial {
    fn get_bsdf<T: TextureEvaluator>(
        &self,
        tex_eval: &T,
        ctx: &MaterialEvalContext,
        lambda: &SampledWavelengths,
    ) -> BSDF {
        let r = SampledSpectrum::clamp(
            &tex_eval.evaluate_spectrum(&self.reflectance, ctx, lambda),
            0.,
            1.,
        );

        let mut u_rough = tex_eval.evaluate_float(&self.u_roughness, ctx);
        let mut v_rough = tex_eval.evaluate_float(&self.v_roughness, ctx);

        if self.remap_roughness {
            u_rough = TrowbridgeReitzDistribution::roughness_to_alpha(u_rough);
            v_rough = TrowbridgeReitzDistribution::roughness_to_alpha(v_rough);
        }

        let distrib = TrowbridgeReitzDistribution::new(u_rough, v_rough);

        let thick = tex_eval.evaluate_float(&self.thickness, ctx);
        let mut sampled_eta = self.eta.evaluate(lambda[0]);
        if self.eta.is_constant() {
            let mut lambda = *lambda;
            lambda.terminate_secondary_inplace();
        }

        if sampled_eta == 0. {
            sampled_eta = 1.
        }

        let a = SampledSpectrum::clamp(
            &tex_eval.evaluate_spectrum(&self.albedo, ctx, lambda),
            0.,
            1.,
        );

        let gg = clamp(tex_eval.evaluate_float(&self.g, ctx), -1., 1.);
        let bxdf = BxDF::CoatedDiffuse(CoatedDiffuseBxDF::new(
            DielectricBxDF::new(sampled_eta, distrib),
            DiffuseBxDF::new(r),
            thick,
            a,
            gg,
            self.max_depth,
            self.n_samples,
            self.seed,
        ));

        BSDF::new(ctx.ns, ctx.dpdus, Ptr::from(&bxdf))
    }

    fn get_bssrdf<T>(
        &self,
        _tex_eval: &T,
        _ctx: &MaterialEvalContext,
        _lambda: &SampledWavelengths,
    ) -> Option<BSSRDF> {
        None
    }

    fn can_evaluate_textures(&self, tex_eval: &dyn TextureEvaluator) -> bool {
        tex_eval.can_evaluate(
            &[self.u_roughness, self.v_roughness, self.thickness, self.g],
            &[self.reflectance, self.albedo],
        )
    }

    fn get_normal_map(&self) -> Option<&Image> {
        Some(&*self.normal_map)
    }

    fn get_displacement(&self) -> Ptr<GPUFloatTexture> {
        self.displacement
    }

    fn has_subsurface_scattering(&self) -> bool {
        false
    }
}

#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct CoatedConductorMaterial {
    displacement: Ptr<GPUFloatTexture>,
    interface_uroughness: Ptr<GPUFloatTexture>,
    interface_vroughness: Ptr<GPUFloatTexture>,
    thickness: Ptr<GPUFloatTexture>,
    interface_eta: Ptr<Spectrum>,
    g: Ptr<GPUFloatTexture>,
    albedo: Ptr<GPUSpectrumTexture>,
    conductor_uroughness: Ptr<GPUFloatTexture>,
    conductor_vroughness: Ptr<GPUFloatTexture>,
    conductor_eta: Ptr<GPUSpectrumTexture>,
    k: Ptr<GPUSpectrumTexture>,
    reflectance: Ptr<GPUSpectrumTexture>,
    normal_map: Ptr<Image>,
    max_depth: u32,
    n_samples: u32,
    remap_roughness: bool,
    seed: i32,
}

impl CoatedConductorMaterial {
    #[allow(clippy::too_many_arguments)]
    pub fn new(
        displacement: Ptr<GPUFloatTexture>,
        interface_uroughness: Ptr<GPUFloatTexture>,
        interface_vroughness: Ptr<GPUFloatTexture>,
        thickness: Ptr<GPUFloatTexture>,
        g: Ptr<GPUFloatTexture>,
        albedo: Ptr<GPUSpectrumTexture>,
        conductor_uroughness: Ptr<GPUFloatTexture>,
        conductor_vroughness: Ptr<GPUFloatTexture>,
        conductor_eta: Ptr<GPUSpectrumTexture>,
        k: Ptr<GPUSpectrumTexture>,
        reflectance: Ptr<GPUSpectrumTexture>,
        normal_map: Ptr<Image>,
        interface_eta: Ptr<Spectrum>,
        max_depth: u32,
        n_samples: u32,
        remap_roughness: bool,
        seed: i32,
    ) -> Self {
        Self {
            displacement,
            normal_map,
            interface_uroughness,
            interface_vroughness,
            thickness,
            interface_eta,
            g,
            albedo,
            conductor_uroughness,
            conductor_vroughness,
            conductor_eta,
            k,
            reflectance,
            remap_roughness,
            max_depth,
            n_samples,
            seed,
        }
    }
}

impl MaterialTrait for CoatedConductorMaterial {
    fn get_bsdf<T: TextureEvaluator>(
        &self,
        tex_eval: &T,
        ctx: &MaterialEvalContext,
        lambda: &SampledWavelengths,
    ) -> BSDF {
        let mut iurough = tex_eval.evaluate_float(&self.interface_uroughness, ctx);
        let mut ivrough = tex_eval.evaluate_float(&self.interface_vroughness, ctx);

        if self.remap_roughness {
            iurough = TrowbridgeReitzDistribution::roughness_to_alpha(iurough);
            ivrough = TrowbridgeReitzDistribution::roughness_to_alpha(ivrough);
        }
        let interface_distrib = TrowbridgeReitzDistribution::new(iurough, ivrough);
        let thick = tex_eval.evaluate_float(&self.thickness, ctx);

        let mut ieta = self.interface_eta.evaluate(lambda[0]);
        if self.interface_eta.is_constant() {
            let mut lambda = *lambda;
            lambda.terminate_secondary_inplace();
        }

        if ieta == 0. {
            ieta = 1.;
        }

        let (mut ce, mut ck) = if !self.conductor_eta.is_null() {
            let k_tex = self.k;
            let ce = tex_eval.evaluate_spectrum(&self.conductor_eta, ctx, lambda);
            let ck = tex_eval.evaluate_spectrum(unsafe { k_tex.as_ref() }, ctx, lambda);
            (ce, ck)
        } else {
            let r = SampledSpectrum::clamp(
                &tex_eval.evaluate_spectrum(&self.reflectance, ctx, lambda),
                0.,
                0.9999,
            );
            let ce = SampledSpectrum::new(1.0);
            let one_minus_r = SampledSpectrum::new(1.) - r;
            let ck = 2. * r.sqrt() / SampledSpectrum::clamp_zero(&one_minus_r).sqrt();
            (ce, ck)
        };

        ce /= ieta;
        ck /= ieta;

        let mut curough = tex_eval.evaluate_float(&self.conductor_uroughness, ctx);
        let mut cvrough = tex_eval.evaluate_float(&self.conductor_vroughness, ctx);

        if self.remap_roughness {
            curough = TrowbridgeReitzDistribution::roughness_to_alpha(curough);
            cvrough = TrowbridgeReitzDistribution::roughness_to_alpha(cvrough);
        }

        let conductor_distrib = TrowbridgeReitzDistribution::new(curough, cvrough);
        let a = SampledSpectrum::clamp(
            &tex_eval.evaluate_spectrum(&self.albedo, ctx, lambda),
            0.,
            1.,
        );

        let gg = clamp(tex_eval.evaluate_float(&self.g, ctx), -1., 1.);
        let bxdf = BxDF::CoatedConductor(CoatedConductorBxDF::new(
            DielectricBxDF::new(ieta, interface_distrib),
            ConductorBxDF::new(&conductor_distrib, ce, ck),
            thick,
            a,
            gg,
            self.max_depth,
            self.n_samples,
            self.seed,
        ));
        BSDF::new(ctx.ns, ctx.dpdus, Ptr::from(&bxdf))
    }

    fn get_bssrdf<T>(
        &self,
        _tex_eval: &T,
        _ctx: &MaterialEvalContext,
        _lambda: &SampledWavelengths,
    ) -> Option<BSSRDF> {
        None
    }

    fn can_evaluate_textures(&self, tex_eval: &dyn TextureEvaluator) -> bool {
        let float_textures = [
            self.interface_uroughness,
            self.interface_vroughness,
            self.thickness,
            self.g,
            self.conductor_uroughness,
            self.conductor_vroughness,
        ];

        let mut spectrum_textures = [Ptr::null(); 4];
        let mut n = 0;

        spectrum_textures[n] = self.albedo;
        n += 1;

        if !self.conductor_eta.is_null() {
            spectrum_textures[n] = self.conductor_eta;
            n += 1;
        }

        if !self.k.is_null() {
            spectrum_textures[n] = self.k;
            n += 1;
        }

        if !self.conductor_eta.is_null() {
            spectrum_textures[n] = self.reflectance;
        }

        tex_eval.can_evaluate(&float_textures, &spectrum_textures)
    }

    fn get_normal_map(&self) -> Option<&Image> {
        Some(&*self.normal_map)
    }

    fn get_displacement(&self) -> Ptr<GPUFloatTexture> {
        self.displacement
    }

    fn has_subsurface_scattering(&self) -> bool {
        false
    }
}
