use crate::core::bsdf::{BSDF, BSDFSample};
use crate::core::bxdf::{BxDFFlags, BxDFReflTransFlags, BxDFTrait, FArgs, TransportMode};
use crate::core::color::RGB;
use crate::core::geometry::{
    Normal3f, Point2f, Vector3f, abs_cos_theta, cos_theta, same_hemisphere,
};
use crate::core::scattering::{
    TrowbridgeReitzDistribution, fr_complex_from_spectrum, fr_dielectric, fresnel_moment1, reflect,
    refract,
};
use crate::spectra::{DeviceStandardColorSpaces, RGBUnboundedSpectrum, SampledSpectrum};
use crate::utils::gpu_array_from_fn;
use crate::utils::math::{
    clamp, fast_exp, i0, lerp, log_i0, radians, safe_acos, safe_asin, safe_sqrt, sample_discrete,
    square, trimmed_logistic,
};
use crate::utils::sampling::{
    cosine_hemisphere_pdf, sample_cosine_hemisphere, sample_trimmed_logistic,
};
use crate::{Float, INV_2_PI, INV_PI, PI};
use core::any::Any;
use num_traits::Float as NumFloat;

static P_MAX: usize = 3;
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct HairBxDF {
    pub h: Float,
    pub eta: Float,
    pub sigma_a: SampledSpectrum,
    pub beta_m: Float,
    pub beta_n: Float,
    pub v: [Float; P_MAX + 1],
    pub s: Float,
    pub sin_2k_alpha: [Float; P_MAX],
    pub cos_2k_alpha: [Float; P_MAX],
    pub colorspaces: DeviceStandardColorSpaces,
}

impl HairBxDF {
    pub fn new(
        h: Float,
        eta: Float,
        sigma_a: SampledSpectrum,
        beta_m: Float,
        beta_n: Float,
        alpha: Float,
        colorspaces: DeviceStandardColorSpaces,
    ) -> Self {
        let mut sin_2k_alpha = [0.; P_MAX];
        let mut cos_2k_alpha = [0.; P_MAX];
        sin_2k_alpha[0] = radians(alpha).sin();
        cos_2k_alpha[0] = safe_sqrt(1. - square(sin_2k_alpha[0]));

        for i in 0..P_MAX {
            sin_2k_alpha[i] = 2. * cos_2k_alpha[i - 1] * sin_2k_alpha[i - 1];
            cos_2k_alpha[i] = square(cos_2k_alpha[i - 1]) - square(sin_2k_alpha[i - 1]);
        }

        Self {
            h,
            eta,
            sigma_a,
            beta_m,
            beta_n,
            v: [0.; P_MAX + 1],
            s: 0.,
            sin_2k_alpha,
            cos_2k_alpha,
            colorspaces,
        }
    }

    fn ap(
        cos_theta_o: Float,
        eta: Float,
        h: Float,
        t: SampledSpectrum,
    ) -> [SampledSpectrum; P_MAX + 1] {
        let cos_gamma_o = safe_sqrt(1. - square(h));
        let cos_theta = cos_theta_o * cos_gamma_o;
        let f = fr_dielectric(cos_theta, eta);
        let ap0 = SampledSpectrum::new(f);
        let ap1 = t * (1.0 - f).powi(2);
        let tf = t * f;
        gpu_array_from_fn(|p| match p {
            0 => ap0,
            1 => ap1,
            _ if p < P_MAX => ap1 * tf.pow_int(p - 1),
            _ => ap1 * tf.pow_int(p - 1) / (SampledSpectrum::new(1.0) - tf),
        })
    }

    fn mp(
        cos_theta_i: Float,
        cos_theta_o: Float,
        sin_theta_i: Float,
        sin_theta_o: Float,
        v: Float,
    ) -> Float {
        let a = cos_theta_i * cos_theta_o / v;
        let b = sin_theta_i * sin_theta_o / v;
        if v <= 0.1 {
            fast_exp(log_i0(a) - b - 1. / v + 0.6931 + (1. / (2. * v).ln()))
        } else {
            fast_exp(-b) * i0(a) / ((1. / v).sinh() * 2. * v)
        }
    }

    fn np(phi: Float, p: i32, s: Float, gamma_o: Float, gamma_t: Float) -> Float {
        let mut dphi = phi - Self::phi(p, gamma_o, gamma_t);
        while dphi > PI {
            dphi -= 2. * PI;
        }
        while dphi < -PI {
            dphi += 2. * PI;
        }

        trimmed_logistic(dphi, s, -PI, PI)
    }

    fn phi(p: i32, gamma_o: Float, gamma_t: Float) -> Float {
        2. * p as Float * gamma_t - 2. * gamma_o + p as Float * PI
    }

    fn ap_pdf(&self, cos_theta_o: Float) -> [Float; P_MAX + 1] {
        let sin_theta_o = safe_sqrt(1. - square(cos_theta_o));
        let sin_theta_t = sin_theta_o / self.eta;
        let cos_theta_t = safe_sqrt(1. - square(sin_theta_t));
        let etap = safe_sqrt(square(self.eta) - square(sin_theta_o)) / cos_theta_o;
        let sin_gamma_t = self.h / etap;
        let cos_gamma_t = safe_sqrt(1. - square(sin_gamma_t));
        // let gamma_t = safe_asin(sin_gamma_t);
        let t_value = -self.sigma_a * (2. * cos_gamma_t / cos_theta_t);
        let t = t_value.exp();
        let ap = Self::ap(cos_theta_o, self.eta, self.h, t);
        let sum_y: Float = ap.iter().map(|s| s.average()).sum();
        gpu_array_from_fn(|i| ap[i].average() / sum_y)
    }

    pub fn sigma_a_from_concentration(
        ce: Float,
        cp: Float,
        stdcs: DeviceStandardColorSpaces,
    ) -> RGBUnboundedSpectrum {
        let eumelanin_sigma_a = RGB::new(0.419, 0.697, 1.37);
        let pheomelanin_sigma_a = RGB::new(0.187, 0.4, 1.05);
        let sigma_a = ce * eumelanin_sigma_a + cp * pheomelanin_sigma_a;
        RGBUnboundedSpectrum::new(&stdcs.srgb, sigma_a)
    }
}

impl BxDFTrait for HairBxDF {
    fn flags(&self) -> BxDFFlags {
        BxDFFlags::GLOSSY_REFLECTION
    }

    fn f(&self, wo: Vector3f, wi: Vector3f, _mode: TransportMode) -> SampledSpectrum {
        // Compute hair coordinate system terms related to wo
        let sin_theta_o = wo.x();
        let cos_theta_o = safe_sqrt(1. - square(sin_theta_o));
        let phi_o = wo.z().atan2(wo.y());
        let gamma_o = safe_asin(self.h);
        // Compute hair coordinate system terms related to wi
        let sin_theta_i = wi.x();
        let cos_theta_i = safe_sqrt(1. - square(sin_theta_i));
        let phi_i = wi.z().atan2(wi.y());

        let sin_theta_t = sin_theta_o / self.eta;
        let cos_theta_t = safe_sqrt(1. - square(sin_theta_t));
        let etap = safe_sqrt(square(self.eta) - square(sin_theta_o)) / cos_theta_o;
        let sin_gamma_t = self.h / etap;
        let cos_gamma_t = safe_sqrt(1. - square(sin_gamma_t));
        let gamma_t = safe_asin(sin_gamma_t);
        let sampled_value = -self.sigma_a * (2. * cos_gamma_t / cos_theta_t);
        let t = sampled_value.exp();
        let phi = phi_i - phi_o;
        let ap_pdf = Self::ap(cos_theta_o, self.eta, self.h, t);
        let mut f_sum = SampledSpectrum::new(0.);
        for (p, &ap) in ap_pdf.iter().enumerate().take(P_MAX) {
            let (sin_thetap_o, cos_thetap_o) = match p {
                0 => (
                    sin_theta_o * self.cos_2k_alpha[1] - cos_theta_o * self.sin_2k_alpha[1],
                    cos_theta_o * self.cos_2k_alpha[1] + sin_theta_o * self.sin_2k_alpha[1],
                ),
                1 => (
                    sin_theta_o * self.cos_2k_alpha[0] + cos_theta_o * self.sin_2k_alpha[0],
                    cos_theta_o * self.cos_2k_alpha[0] - sin_theta_o * self.sin_2k_alpha[0],
                ),
                2 => (
                    sin_theta_o * self.cos_2k_alpha[2] + cos_theta_o * self.sin_2k_alpha[2],
                    cos_theta_o * self.cos_2k_alpha[2] - sin_theta_o * self.sin_2k_alpha[2],
                ),
                _ => (sin_theta_o, cos_theta_o),
            };

            f_sum += Self::mp(
                cos_theta_i,
                cos_thetap_o,
                sin_theta_i,
                sin_thetap_o,
                self.v[p],
            ) * ap
                * Self::np(phi, p as i32, self.s, gamma_o, gamma_t);
        }
        if abs_cos_theta(wi) > 0. {
            f_sum /= abs_cos_theta(wi);
        }
        f_sum
    }

    fn sample_f(
        &self,
        wo: Vector3f,
        mut uc: Float,
        u: Point2f,
        f_args: FArgs,
    ) -> Option<BSDFSample> {
        let sin_theta_o = wo.x();
        let cos_theta_o = safe_sqrt(1. - square(sin_theta_o));
        let phi_o = wo.z().atan2(wo.y());
        let gamma_o = safe_asin(self.h);
        // Determine which term to sample for hair scattering
        let ap_pdf = self.ap_pdf(cos_theta_o);
        let p = sample_discrete(&ap_pdf, uc, None, Some(&mut uc));
        let (sin_thetap_o, mut cos_thetap_o) = match p {
            0 => (
                sin_theta_o * self.cos_2k_alpha[1] - cos_theta_o * self.sin_2k_alpha[1],
                cos_theta_o * self.cos_2k_alpha[1] + sin_theta_o * self.sin_2k_alpha[1],
            ),
            1 => (
                sin_theta_o * self.cos_2k_alpha[0] + cos_theta_o * self.sin_2k_alpha[0],
                cos_theta_o * self.cos_2k_alpha[0] - sin_theta_o * self.sin_2k_alpha[0],
            ),
            2 => (
                sin_theta_o * self.cos_2k_alpha[2] + cos_theta_o * self.sin_2k_alpha[2],
                cos_theta_o * self.cos_2k_alpha[2] - sin_theta_o * self.sin_2k_alpha[2],
            ),
            _ => (sin_theta_o, cos_theta_o),
        };

        cos_thetap_o = cos_thetap_o.abs();
        let cos_theta =
            1. + self.v[p] * (u[0].max(1e-5) + (1. - u[0]) * fast_exp(-2. / self.v[p])).ln();
        let sin_theta = safe_sqrt(1. - square(cos_theta));
        let cos_phi = (2. * PI * u[1]).cos();
        let sin_theta_i = -cos_theta * sin_thetap_o + sin_theta * cos_phi * cos_thetap_o;
        let cos_theta_i = safe_sqrt(1. - square(sin_theta_i));
        let etap = safe_sqrt(square(self.eta) - square(sin_theta_o)) / cos_theta_o;
        let sin_gamma_t = self.h / etap;
        // let cos_gamma_t = safe_sqrt(1. - square(sin_gamma_t));
        let gamma_t = safe_asin(sin_gamma_t);
        let dphi = if p < P_MAX {
            Self::phi(p as i32, gamma_o, gamma_t) + sample_trimmed_logistic(uc, self.s, -PI, PI)
        } else {
            2. * PI * uc
        };
        let phi_i = phi_o + dphi;
        let wi = Vector3f::new(
            sin_theta_i,
            cos_theta_i * phi_i.cos(),
            cos_theta_i * phi_i.sin(),
        );

        let mut pdf = 0.;
        for (p, &ap) in ap_pdf.iter().enumerate().take(P_MAX) {
            let (sin_thetap_o, cos_thetap_o_raw) = match p {
                0 => (
                    sin_theta_o * self.cos_2k_alpha[1] - cos_theta_o * self.sin_2k_alpha[1],
                    cos_theta_o * self.cos_2k_alpha[1] + sin_theta_o * self.sin_2k_alpha[1],
                ),
                1 => (
                    sin_theta_o * self.cos_2k_alpha[0] + cos_theta_o * self.sin_2k_alpha[0],
                    cos_theta_o * self.cos_2k_alpha[0] - sin_theta_o * self.sin_2k_alpha[0],
                ),
                2 => (
                    sin_theta_o * self.cos_2k_alpha[2] + cos_theta_o * self.sin_2k_alpha[2],
                    cos_theta_o * self.cos_2k_alpha[2] - sin_theta_o * self.sin_2k_alpha[2],
                ),
                _ => (sin_theta_o, cos_theta_o),
            };
            let cos_thetap_o = cos_thetap_o_raw.abs();
            pdf += Self::mp(
                cos_theta_i,
                cos_thetap_o,
                sin_theta_i,
                sin_thetap_o,
                self.v[p],
            ) * ap
                * Self::np(dphi, p as i32, self.s, gamma_o, gamma_t);
        }
        pdf += Self::mp(
            cos_theta_i,
            cos_theta_o,
            sin_theta_i,
            sin_theta_o,
            self.v[P_MAX],
        ) * ap_pdf[P_MAX]
            * INV_2_PI;

        let bsd = BSDFSample {
            f: self.f(wo, wi, f_args.mode),
            wi,
            pdf,
            flags: self.flags(),
            ..Default::default()
        };

        Some(bsd)
    }

    fn pdf(&self, wo: Vector3f, wi: Vector3f, _f_args: FArgs) -> Float {
        let sin_theta_o = wo.x();
        let cos_theta_o = safe_sqrt(1. - square(sin_theta_o));
        let phi_o = wo.z().atan2(wo.y());
        let gamma_o = safe_asin(self.h);
        // Determine which term to sample for hair scattering
        let sin_theta_i = wi.x();
        let cos_theta_i = safe_sqrt(1. - square(sin_theta_i));
        let phi_i = wi.z().atan2(wi.y());
        // Compute $\gammat$ for refracted ray
        let etap = safe_sqrt(self.eta * self.eta - square(sin_theta_o)) / cos_theta_o;
        let sin_gamma_t = self.h / etap;
        let gamma_t = safe_asin(sin_gamma_t);
        // Compute PDF for $A_p$ terms
        let ap_pdf = self.ap_pdf(cos_theta_o);
        let phi = phi_i - phi_o;

        let mut pdf = 0.;
        for (p, &ap) in ap_pdf.iter().enumerate().take(P_MAX) {
            let (sin_thetap_o, raw_cos_thetap_o) = match p {
                0 => (
                    sin_theta_o * self.cos_2k_alpha[1] - cos_theta_o * self.sin_2k_alpha[1],
                    cos_theta_o * self.cos_2k_alpha[1] + sin_theta_o * self.sin_2k_alpha[1],
                ),
                1 => (
                    sin_theta_o * self.cos_2k_alpha[0] + cos_theta_o * self.sin_2k_alpha[0],
                    cos_theta_o * self.cos_2k_alpha[0] - sin_theta_o * self.sin_2k_alpha[0],
                ),
                2 => (
                    sin_theta_o * self.cos_2k_alpha[2] + cos_theta_o * self.sin_2k_alpha[2],
                    cos_theta_o * self.cos_2k_alpha[2] - sin_theta_o * self.sin_2k_alpha[2],
                ),
                _ => (sin_theta_o, cos_theta_o),
            };

            let cos_thetap_o = raw_cos_thetap_o.abs();

            pdf += Self::mp(
                cos_theta_i,
                cos_thetap_o,
                sin_theta_i,
                sin_thetap_o,
                self.v[p],
            ) * ap
                * Self::np(phi, p as i32, self.s, gamma_o, gamma_t);
        }

        pdf += Self::mp(
            cos_theta_i,
            cos_theta_o,
            sin_theta_i,
            sin_theta_o,
            self.v[P_MAX],
        ) * ap_pdf[P_MAX]
            * INV_2_PI;
        pdf
    }

    fn as_any(&self) -> &dyn Any {
        self
    }
}

#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct NormalizedFresnelBxDF {
    pub eta: Float,
}

impl BxDFTrait for NormalizedFresnelBxDF {
    fn f(&self, wo: Vector3f, wi: Vector3f, mode: TransportMode) -> SampledSpectrum {
        if !same_hemisphere(wo, wi) {
            return SampledSpectrum::new(0.);
        }

        let c = 1. - 2. * fresnel_moment1(1. / self.eta);
        let mut f = SampledSpectrum::new((1. - fr_dielectric(cos_theta(wi), self.eta)) / (c * PI));
        if mode == TransportMode::Radiance {
            f /= square(self.eta)
        }
        f
    }

    fn sample_f(&self, wo: Vector3f, _uc: Float, u: Point2f, f_args: FArgs) -> Option<BSDFSample> {
        let reflection_flags =
            BxDFReflTransFlags::from_bits_truncate(BxDFReflTransFlags::REFLECTION.bits());
        if !f_args.sample_flags.contains(reflection_flags) {
            return None;
        }
        let mut wi = sample_cosine_hemisphere(u);
        if wo.z() < 0. {
            wi[2] *= -1.;
        }

        Some(BSDFSample {
            f: self.f(wo, wi, f_args.mode),
            wi,
            pdf: self.pdf(wo, wi, f_args),
            flags: BxDFFlags::DIFFUSE_REFLECTION,
            ..Default::default()
        })
    }

    fn pdf(&self, wo: Vector3f, wi: Vector3f, f_args: FArgs) -> Float {
        let reflection_flags =
            BxDFReflTransFlags::from_bits_truncate(BxDFReflTransFlags::REFLECTION.bits());
        if !f_args.sample_flags.contains(reflection_flags) {
            return 0.;
        }

        if !same_hemisphere(wo, wi) {
            return 0.;
        }

        abs_cos_theta(wi) * INV_PI
    }

    fn flags(&self) -> BxDFFlags {
        BxDFFlags::REFLECTION | BxDFFlags::DIFFUSE
    }

    fn regularize(&mut self) {}

    fn as_any(&self) -> &dyn Any {
        self
    }
}

#[derive(Debug)]
pub struct EmptyBxDF;
impl BxDFTrait for EmptyBxDF {
    fn f(&self, _wo: Vector3f, _wi: Vector3f, _mode: TransportMode) -> SampledSpectrum {
        SampledSpectrum::default()
    }

    fn sample_f(
        &self,
        _wo: Vector3f,
        _u: Float,
        _u2: Point2f,
        _f_args: FArgs,
    ) -> Option<BSDFSample> {
        None
    }

    fn pdf(&self, _wo: Vector3f, _wi: Vector3f, _f_args: FArgs) -> Float {
        0.0
    }

    fn flags(&self) -> BxDFFlags {
        BxDFFlags::UNSET
    }

    fn regularize(&mut self) {}

    fn as_any(&self) -> &dyn Any {
        self
    }
}
