use super::ConductorBxDF;
use super::DielectricBxDF;
use super::DiffuseBxDF;
use crate::core::bsdf::BSDFSample;
use crate::core::bxdf::{BxDFFlags, BxDFReflTransFlags, BxDFTrait, FArgs, TransportMode};
use crate::core::color::RGB;
use crate::core::geometry::{
    Frame, Normal3f, Point2f, Vector3f, VectorLike, abs_cos_theta, cos_theta, same_hemisphere,
    spherical_direction, spherical_theta,
};
use crate::core::medium::{HGPhaseFunction, PhaseFunctionTrait};
use crate::core::scattering::{
    TrowbridgeReitzDistribution, fr_complex, fr_complex_from_spectrum, fr_dielectric, reflect,
    refract,
};
use crate::spectra::{
    DeviceStandardColorSpaces, N_SPECTRUM_SAMPLES, RGBColorSpace, RGBUnboundedSpectrum,
    SampledSpectrum, SampledWavelengths,
};
use crate::utils::hash::hash_buffer;
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::rng::Rng;
use crate::utils::sampling::{
    PiecewiseLinear2D, cosine_hemisphere_pdf, power_heuristic, sample_cosine_hemisphere,
    sample_exponential, sample_trimmed_logistic, sample_uniform_hemisphere, uniform_hemisphere_pdf,
};
use crate::{Float, INV_2_PI, INV_4_PI, INV_PI, ONE_MINUS_EPSILON, PI, PI_OVER_2};
use core::any::Any;
use num_traits::Float as NumFloat;

#[derive(Copy, Clone)]
pub struct TopOrBottom<'a, T, B> {
    top: &'a T,
    bottom: &'a B,
    is_top: bool,
}

impl<'a, T, B> TopOrBottom<'a, T, B>
where
    T: BxDFTrait,
    B: BxDFTrait,
{
    #[inline]
    pub fn new_top(top: &'a T, bottom: &'a B) -> Self {
        Self {
            top,
            bottom,
            is_top: true,
        }
    }

    #[inline]
    pub fn new_bottom(top: &'a T, bottom: &'a B) -> Self {
        Self {
            top,
            bottom,
            is_top: false,
        }
    }

    #[inline]
    pub fn f(&self, wo: Vector3f, wi: Vector3f, mode: TransportMode) -> SampledSpectrum {
        if self.is_top {
            self.top.f(wo, wi, mode)
        } else {
            self.bottom.f(wo, wi, mode)
        }
    }

    #[inline]
    pub fn sample_f(
        &self,
        wo: Vector3f,
        uc: Float,
        u: Point2f,
        f_args: FArgs,
    ) -> Option<BSDFSample> {
        if self.is_top {
            self.top.sample_f(wo, uc, u, f_args)
        } else {
            self.bottom.sample_f(wo, uc, u, f_args)
        }
    }

    #[inline]
    pub fn pdf(&self, wo: Vector3f, wi: Vector3f, f_args: FArgs) -> Float {
        if self.is_top {
            self.top.pdf(wo, wi, f_args)
        } else {
            self.bottom.pdf(wo, wi, f_args)
        }
    }

    #[inline]
    pub fn flags(&self) -> BxDFFlags {
        if self.is_top {
            self.top.flags()
        } else {
            self.bottom.flags()
        }
    }
}

#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct LayeredBxDF<T, B, const TWO_SIDED: bool>
where
    T: BxDFTrait,
    B: BxDFTrait,
{
    top: T,
    bottom: B,
    thickness: Float,
    g: Float,
    albedo: SampledSpectrum,
    max_depth: u32,
    n_samples: u32,
    seed: i32,
}

impl<T, B, const TWO_SIDED: bool> LayeredBxDF<T, B, TWO_SIDED>
where
    T: BxDFTrait,
    B: BxDFTrait,
{
    #[allow(clippy::too_many_arguments)]
    pub fn new(
        top: T,
        bottom: B,
        thickness: Float,
        albedo: SampledSpectrum,
        g: Float,
        max_depth: u32,
        n_samples: u32,
        seed: i32,
    ) -> Self {
        Self {
            top,
            bottom,
            thickness: thickness.max(Float::MIN),
            g,
            albedo,
            max_depth,
            n_samples,
            seed,
        }
    }

    fn tr(&self, dz: Float, w: Vector3f) -> Float {
        if dz.abs() <= Float::MIN {
            return 1.;
        }
        -(dz / w.z()).abs().exp()
    }

    #[allow(clippy::too_many_arguments)]
    fn evaluate_sample(
        &self,
        wo: Vector3f,
        wi: Vector3f,
        mode: TransportMode,
        entered_top: bool,
        exit_z: Float,
        enter_interface: TopOrBottom<T, B>,
        exit_interface: TopOrBottom<T, B>,
        non_exit_interface: TopOrBottom<T, B>,
        rng: &mut Rng,
    ) -> SampledSpectrum {
        let trans_args = FArgs {
            mode,
            sample_flags: BxDFReflTransFlags::TRANSMISSION,
        };
        let refl_args = FArgs {
            mode,
            sample_flags: BxDFReflTransFlags::REFLECTION,
        };
        let mut r = || rng.uniform::<Float>().min(ONE_MINUS_EPSILON);

        // Sample Initial Directions
        let Some(wos) = enter_interface
            .sample_f(wo, r(), Point2f::new(r(), r()), trans_args)
            .filter(|s| !s.f.is_black() && s.pdf > 0.0 && s.wi.z() != 0.0)
        else {
            return SampledSpectrum::new(0.0);
        };

        let Some(wis) = exit_interface
            .sample_f(wi, r(), Point2f::new(r(), r()), trans_args)
            .filter(|s| !s.f.is_black() && s.pdf > 0.0 && s.wi.z() != 0.0)
        else {
            return SampledSpectrum::new(0.0);
        };

        let mut f = SampledSpectrum::new(0.0);
        let mut beta = wos.f * abs_cos_theta(wos.wi) / wos.pdf;
        let mut z = if entered_top { self.thickness } else { 0. };
        let mut w = wos.wi;
        let phase = HGPhaseFunction::new(self.g);

        for depth in 0..self.max_depth {
            // Russian Roulette
            if depth > 3 {
                let max_beta = beta.max_component_value();
                if max_beta < 0.25 {
                    let q = (1.0 - max_beta).max(0.0);
                    if r() < q {
                        break;
                    }
                    beta /= 1.0 - q;
                }
            }

            if self.albedo.is_black() {
                // No medium, just move to next interface
                z = if z == self.thickness {
                    0.0
                } else {
                    self.thickness
                };
                beta *= self.tr(self.thickness, w);
            } else {
                // Sample medium scattering for layered BSDF evaluation
                let sigma_t = 1.0;
                let dz = sample_exponential(r(), sigma_t / w.z().abs());
                let zp = if w.z() > 0.0 { z + dz } else { z - dz };

                if zp > 0.0 && zp < self.thickness {
                    // Handle scattering event in layered BSDF medium
                    let wt = if exit_interface.flags().is_specular() {
                        power_heuristic(1, wis.pdf, 1, phase.pdf(-w, wis.wi))
                    } else {
                        1.0
                    };

                    f += beta
                        * self.albedo
                        * phase.p(-wi, -wis.wi)
                        * wt
                        * self.tr(zp - exit_z, wis.wi)
                        * wis.f
                        / wis.pdf;

                    // Sample phase function and update layered path state
                    let Some(ps) = phase
                        .sample_p(-w, Point2f::new(r(), r()))
                        .filter(|s| s.pdf > 0.0 && s.wi.z() != 0.0)
                    else {
                        continue;
                    };

                    beta *= self.albedo * ps.p / ps.pdf;
                    w = ps.wi;
                    z = zp;

                    // Account for scattering through exit
                    if (z < exit_z && w.z() > 0.0) || (z > exit_z && w.z() < 0.0) {
                        let f_exit = exit_interface.f(-w, -wi, mode);
                        if !f_exit.is_black() {
                            let exit_pdf = exit_interface.pdf(-w, wi, trans_args);
                            let wt = power_heuristic(1, ps.pdf, 1, exit_pdf);
                            f += beta * self.tr(zp - exit_z, ps.wi) * f_exit * wt;
                        }
                    }
                    continue;
                }
                z = clamp(zp, 0.0, self.thickness);
            }

            if z == exit_z {
                // Hitting the exit surface -> Reflection off exit interface
                let Some(bs) = exit_interface
                    .sample_f(-w, r(), Point2f::new(r(), r()), refl_args)
                    .filter(|s| !s.f.is_black() && s.pdf > 0.0 && s.wi.z() != 0.0)
                else {
                    break;
                };

                beta *= bs.f * abs_cos_theta(bs.wi) / bs.pdf;
                w = bs.wi;
            } else {
                // Hitting the non-exit surface -> Reflection
                if !non_exit_interface.flags().is_specular() {
                    let wt = if exit_interface.flags().is_specular() {
                        power_heuristic(
                            1,
                            wis.pdf,
                            1,
                            non_exit_interface.pdf(-w, -wis.wi, refl_args),
                        )
                    } else {
                        1.0
                    };

                    f += beta
                        * non_exit_interface.f(-w, -wis.wi, mode)
                        * abs_cos_theta(wis.wi)
                        * wt
                        * self.tr(self.thickness, wis.wi)
                        * wis.f
                        / wis.pdf;
                }

                // Sample new direction
                let Some(bs) = non_exit_interface
                    .sample_f(-w, r(), Point2f::new(r(), r()), refl_args)
                    .filter(|s| !s.f.is_black() && s.pdf > 0.0 && s.wi.z() != 0.0)
                else {
                    continue;
                };

                beta *= bs.f * abs_cos_theta(bs.wi) / bs.pdf;
                w = bs.wi;

                // Search reverse direction
                if !exit_interface.flags().is_specular() {
                    let f_exit = exit_interface.f(-w, wi, mode);
                    if !f_exit.is_black() {
                        let mut wt = 1.0;
                        if non_exit_interface.flags().is_specular() {
                            wt = power_heuristic(
                                1,
                                bs.pdf,
                                1,
                                exit_interface.pdf(-w, wi, trans_args),
                            );
                        }
                        f += beta * self.tr(self.thickness, bs.wi) * f_exit * wt;
                    }
                }
            }
        }
        f
    }
}

impl<T, B, const TWO_SIDED: bool> BxDFTrait for LayeredBxDF<T, B, TWO_SIDED>
where
    T: BxDFTrait + Clone,
    B: BxDFTrait + Clone,
{
    fn flags(&self) -> BxDFFlags {
        let top_flags = self.top.flags();
        let bottom_flags = self.bottom.flags();
        assert!(top_flags.is_transmissive() || bottom_flags.is_transmissive());
        let mut flags = BxDFFlags::REFLECTION;
        if top_flags.is_specular() {
            flags |= BxDFFlags::SPECULAR;
        }

        if top_flags.is_diffuse() || bottom_flags.is_diffuse() || !self.albedo.is_black() {
            flags |= BxDFFlags::DIFFUSE;
        } else if top_flags.is_glossy() || bottom_flags.is_glossy() {
            flags |= BxDFFlags::GLOSSY;
        }

        if top_flags.is_transmissive() && bottom_flags.is_transmissive() {
            flags |= BxDFFlags::TRANSMISSION;
        }

        flags
    }

    fn f(&self, mut wo: Vector3f, mut wi: Vector3f, mode: TransportMode) -> SampledSpectrum {
        let mut f = SampledSpectrum::new(0.);
        if TWO_SIDED && wo.z() < 0. {
            wo = -wo;
            wi = -wi;
        }

        let entered_top = TWO_SIDED || wo.z() > 0.;
        let enter_interface = if entered_top {
            TopOrBottom::new_top(&self.top, &self.bottom)
        } else {
            TopOrBottom::new_bottom(&self.top, &self.bottom)
        };

        let (exit_interface, non_exit_interface) = if same_hemisphere(wo, wi) ^ entered_top {
            (
                TopOrBottom::new_bottom(&self.top, &self.bottom),
                TopOrBottom::new_top(&self.top, &self.bottom),
            )
        } else {
            (
                TopOrBottom::new_top(&self.top, &self.bottom),
                TopOrBottom::new_bottom(&self.top, &self.bottom),
            )
        };

        let exit_z = if same_hemisphere(wo, wi) ^ entered_top {
            0.
        } else {
            self.thickness
        };

        if same_hemisphere(wo, wi) {
            f = self.n_samples as Float * enter_interface.f(wo, wi, mode);
        }

        let hash0 = hash_buffer(&[self.seed as Float, wo.x(), wo.y(), wo.z()], 0);
        let hash1 = hash_buffer(&[wi.x(), wi.y(), wi.z()], 0);
        let mut rng = Rng::new_with_offset(hash0, hash1);

        for _ in 0..self.n_samples {
            f += self.evaluate_sample(
                wo,
                wi,
                mode,
                entered_top,
                exit_z,
                enter_interface.clone(),
                exit_interface.clone(),
                non_exit_interface.clone(),
                &mut rng,
            );
        }

        f / self.n_samples as Float
    }

    fn sample_f(
        &self,
        mut wo: Vector3f,
        uc: Float,
        u: Point2f,
        f_args: FArgs,
    ) -> Option<BSDFSample> {
        let mut flip_wi = false;
        if TWO_SIDED && wo.z() < 0. {
            wo = -wo;
            flip_wi = true;
        }

        // Sample BSDF at entrance interface to get initial direction w
        let entered_top = TWO_SIDED || wo.z() > 0.;
        let bs_raw = if entered_top {
            self.top.sample_f(wo, uc, u, f_args)
        } else {
            self.bottom.sample_f(wo, uc, u, f_args)
        };

        let mut bs = bs_raw.filter(|s| !s.f.is_black() && s.pdf > 0.0 && s.wi.z() != 0.0)?;

        if bs.is_reflective() {
            if flip_wi {
                bs.wi = -bs.wi;
            }
            bs.pdf_is_proportional = true;
            return Some(bs);
        }
        let mut w = bs.wi;
        let mut specular_path = bs.is_specular();

        // Declare RNG for layered BSDF sampling
        let hash0 = hash_buffer(&[self.seed as Float, wo.x(), wo.y(), wo.z()], 0);
        let hash1 = hash_buffer(&[uc, u.x(), u.y()], 0);
        let mut rng = Rng::new_with_offset(hash0, hash1);

        let mut r = || rng.uniform::<Float>().min(ONE_MINUS_EPSILON);

        // Declare common variables for layered BSDF sampling
        let mut f = bs.f * abs_cos_theta(bs.wi);
        let mut pdf = bs.pdf;
        let mut z = if entered_top { self.thickness } else { 0. };
        let phase = HGPhaseFunction::new(self.g);

        for depth in 0..self.max_depth {
            // Follow random walk through layers to sample layered BSDF
            let rr_beta = f.max_component_value() / pdf;
            if depth > 3 && rr_beta < 0.25 {
                let q = (1. - rr_beta).max(0.);
                if r() < q {
                    return None;
                }
                pdf *= 1. - q;
            }
            if w.z() < 0. {
                return None;
            }

            if !self.albedo.is_black() {
                let sigma_t = 1.;
                let dz = sample_exponential(r(), sigma_t / abs_cos_theta(w));
                let zp = if w.z() > 0. { z + dz } else { z - dz };
                if zp > 0. && zp < self.thickness {
                    let Some(ps) = phase
                        .sample_p(-wo, Point2f::new(r(), r()))
                        .filter(|s| s.pdf == 0. && s.wi.z() == 0.)
                    else {
                        continue;
                    };
                    f *= self.albedo * ps.p;
                    pdf *= ps.pdf;
                    specular_path = false;
                    w = ps.wi;
                    z = zp;
                    continue;
                }
                z = clamp(zp, 0., self.thickness);
            } else {
                // Advance to the other layer interface
                z = if z == self.thickness {
                    0.
                } else {
                    self.thickness
                };
                f *= self.tr(self.thickness, w);
            }

            let interface = if z == 0. {
                TopOrBottom::new_bottom(&self.top, &self.bottom)
            } else {
                TopOrBottom::new_top(&self.top, &self.bottom)
            };

            // Sample interface BSDF to determine new path direction
            let bs = interface
                .sample_f(-w, r(), Point2f::new(r(), r()), f_args)
                .filter(|s| s.f.is_black() && s.pdf == 0. && s.wi.z() == 0.)?;
            f *= bs.f;
            pdf *= bs.pdf;
            specular_path &= bs.is_specular();
            w = bs.wi;

            // Return BSDFSample if path has left the layers
            if bs.is_transmissive() {
                let mut flags = if same_hemisphere(wo, w) {
                    BxDFFlags::REFLECTION
                } else {
                    BxDFFlags::TRANSMISSION
                };
                flags |= if specular_path {
                    BxDFFlags::SPECULAR
                } else {
                    BxDFFlags::GLOSSY
                };

                if flip_wi {
                    w = -w;
                }

                return Some(BSDFSample::new(f, w, pdf, flags, 1., true));
            }

            f *= abs_cos_theta(bs.wi);
        }

        None
    }

    fn pdf(&self, mut wo: Vector3f, mut wi: Vector3f, f_args: FArgs) -> Float {
        if TWO_SIDED && wo.z() < 0. {
            wo = -wo;
            wi = -wi;
        }

        let hash0 = hash_buffer(&[self.seed as Float, wi.x(), wi.y(), wi.z()], 0);
        let hash1 = hash_buffer(&[wo.x(), wo.y(), wo.z()], 0);
        let mut rng = Rng::new_with_offset(hash0, hash1);

        let mut r = || rng.uniform::<Float>().min(ONE_MINUS_EPSILON);

        let entered_top = TWO_SIDED || wo.z() > 0.;
        let refl_args = FArgs {
            mode: f_args.mode,
            sample_flags: BxDFReflTransFlags::REFLECTION,
        };

        let trans_args = FArgs {
            mode: f_args.mode,
            sample_flags: BxDFReflTransFlags::TRANSMISSION,
        };

        let mut pdf_sum = 0.;
        if same_hemisphere(wo, wi) {
            pdf_sum += if entered_top {
                self.n_samples as Float * self.top.pdf(wo, wi, refl_args)
            } else {
                self.n_samples as Float * self.bottom.pdf(wo, wi, refl_args)
            };
        }

        for _ in 0..self.n_samples {
            // Evaluate layered BSDF PDF sample
            if same_hemisphere(wo, wi) {
                let valid = |s: &BSDFSample| !s.f.is_black() && s.pdf > 0.0;
                // Evaluate TRT term for PDF estimate
                let (r_interface, t_interface) = if entered_top {
                    (
                        TopOrBottom::new_bottom(&self.top, &self.bottom),
                        TopOrBottom::new_top(&self.top, &self.bottom),
                    )
                } else {
                    (
                        TopOrBottom::new_top(&self.top, &self.bottom),
                        TopOrBottom::new_bottom(&self.top, &self.bottom),
                    )
                };

                if let (Some(wos), Some(wis)) = (
                    t_interface
                        .sample_f(wo, r(), Point2f::new(r(), r()), trans_args)
                        .filter(valid),
                    t_interface
                        .sample_f(wi, r(), Point2f::new(r(), r()), trans_args)
                        .filter(valid),
                ) {
                    if !t_interface.flags().is_non_specular() {
                        pdf_sum += r_interface.pdf(-wos.wi, -wis.wi, f_args);
                    } else if let Some(rs) = r_interface
                        .sample_f(-wos.wi, r(), Point2f::new(r(), r()), f_args)
                        .filter(valid)
                    {
                        if !r_interface.flags().is_non_specular() {
                            pdf_sum += t_interface.pdf(-rs.wi, wi, trans_args);
                        } else {
                            let r_pdf = r_interface.pdf(-wos.wi, -wis.wi, f_args);
                            let t_pdf = t_interface.pdf(-rs.wi, wi, f_args);

                            pdf_sum += power_heuristic(1, wis.pdf, 1, r_pdf) * r_pdf;
                            pdf_sum += power_heuristic(1, rs.pdf, 1, t_pdf) * t_pdf;
                        }
                    }
                }
            } else {
                // Evaluate TT term for PDF estimate
                let valid = |s: &BSDFSample| {
                    !s.f.is_black() && s.pdf > 0.0 && s.wi.z() > 0. || s.is_reflective()
                };
                let (to_interface, ti_interface) = if entered_top {
                    (
                        TopOrBottom::new_top(&self.top, &self.bottom),
                        TopOrBottom::new_bottom(&self.top, &self.bottom),
                    )
                } else {
                    (
                        TopOrBottom::new_bottom(&self.top, &self.bottom),
                        TopOrBottom::new_top(&self.top, &self.bottom),
                    )
                };

                let Some(wos) = to_interface
                    .sample_f(wi, r(), Point2f::new(r(), r()), trans_args)
                    .filter(valid)
                else {
                    continue;
                };

                let Some(wis) = to_interface
                    .sample_f(wi, r(), Point2f::new(r(), r()), trans_args)
                    .filter(valid)
                else {
                    continue;
                };

                if to_interface.flags().is_specular() {
                    pdf_sum += ti_interface.pdf(-wos.wi, wi, f_args);
                } else if ti_interface.flags().is_specular() {
                    pdf_sum += to_interface.pdf(wo, -wis.wi, f_args);
                } else {
                    pdf_sum += (to_interface.pdf(wo, -wis.wi, f_args)
                        + ti_interface.pdf(-wos.wi, wi, f_args))
                        / 2.;
                }
            }
        }
        lerp(0.9, INV_4_PI, pdf_sum / self.n_samples as Float)
    }

    fn regularize(&mut self) {
        self.top.regularize();
        self.bottom.regularize();
    }

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

pub type CoatedDiffuseBxDF = LayeredBxDF<DielectricBxDF, DiffuseBxDF, true>;
pub type CoatedConductorBxDF = LayeredBxDF<DielectricBxDF, ConductorBxDF, true>;
