use crate::core::filter::FilterTrait;
use crate::core::geometry::{Bounds2f, Point2f, Point2i, Vector2f};
use crate::core::pbrt::{Float, ONE_MINUS_EPSILON, PI, PI_OVER_2, PI_OVER_4};
use crate::utils::math::{
    clamp, encode_morton_2, inverse_radical_inverse, lerp, log2_int,
    owen_scrambled_radical_inverse, permutation_element, radical_inverse, round_up_pow2,
    scrambled_radical_inverse, sobol_interval_to_index, sobol_sample, BinaryPermuteScrambler,
    DigitPermutation, FastOwenScrambler, NoRandomizer, OwenScrambler, Scrambler,
    PRIME_TABLE_SIZE,
};
use crate::utils::rng::Rng;
use crate::utils::sobol::N_SOBOL_DIMENSIONS;
use crate::utils::{hash::*, sobol};
use crate::{gvec, GVec, Ptr};
use enum_dispatch::enum_dispatch;

#[repr(C)]
#[derive(Debug, Default, Clone, Copy)]
pub struct CameraSample {
    pub p_film: Point2f,
    pub p_lens: Point2f,
    pub time: Float,
    pub filter_weight: Float,
}

pub fn get_camera_sample<S, F>(sampler: &mut S, p_pixel: Point2i, filter: &F) -> CameraSample
where
    S: SamplerTrait,
    F: FilterTrait,
{
    let fs = filter.sample(sampler.get_pixel2d());
    CameraSample {
        p_film: Point2f::from(p_pixel) + Vector2f::from(fs.p) + Vector2f::new(0.5, 0.5),
        time: sampler.get1d(),
        p_lens: sampler.get2d(),
        filter_weight: fs.weight,
    }
}

#[repr(C)]
#[derive(Default, Debug, Clone, Copy)]
pub struct IndependentSampler {
    pub samples_per_pixel: i32,
    pub seed: u64,
    pub rng: Rng,
}

impl IndependentSampler {
    pub fn new(samples_per_pixel: i32, seed: u64) -> Self {
        Self {
            samples_per_pixel,
            seed,
            rng: Rng::default(),
        }
    }
}

impl SamplerTrait for IndependentSampler {
    fn samples_per_pixel(&self) -> i32 {
        self.samples_per_pixel
    }
    fn start_pixel_sample(&mut self, p: Point2i, sample_index: i32, dim: Option<u32>) {
        let hash_input = [p.x() as u64, p.y() as u64, self.seed];
        let sequence_index = hash_buffer(&hash_input, 0);
        self.rng.set_sequence(sequence_index);
        self.rng
            .advance((sample_index as u64) * 65536 + (dim.unwrap_or(0) as u64));
    }

    fn get1d(&mut self) -> Float {
        self.rng.uniform::<Float>()
    }
    fn get2d(&mut self) -> Point2f {
        Point2f::new(self.rng.uniform::<Float>(), self.rng.uniform::<Float>())
    }
    fn get_pixel2d(&mut self) -> Point2f {
        self.get2d()
    }
}

pub const MAX_HALTON_RESOLUTION: i32 = 128;

#[repr(C)]
#[derive(Debug, Default, Clone, PartialEq, Eq, Copy)]
pub enum RandomizeStrategy {
    #[default]
    None,
    PermuteDigits,
    FastOwen,
    Owen,
}

#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct HaltonSampler {
    pub samples_per_pixel: i32,
    pub randomize: RandomizeStrategy,
    pub base_scales: [u64; 2],
    pub base_exponents: [u64; 2],
    pub mult_inverse: [u64; 2],
    pub halton_index: u64,
    pub dim: u32,
    pub digit_permutations: Ptr<DigitPermutation>,
}

#[allow(clippy::derivable_impls)]
impl Default for HaltonSampler {
    fn default() -> Self {
        Self {
            samples_per_pixel: 0,
            randomize: RandomizeStrategy::default(),
            base_scales: [0; 2],
            base_exponents: [0; 2],
            mult_inverse: [0; 2],
            halton_index: 0,
            dim: 0,
            digit_permutations: Ptr::default(),
        }
    }
}

impl HaltonSampler {
    pub fn sample_dimension(&self, dimension: u32) -> Float {
        if self.randomize == RandomizeStrategy::None {
            radical_inverse(dimension, self.halton_index)
        } else if self.randomize == RandomizeStrategy::PermuteDigits {
            let digit_perm = unsafe { &*self.digit_permutations.add(dimension as usize) };
            scrambled_radical_inverse(dimension, self.halton_index, digit_perm)
        } else {
            owen_scrambled_radical_inverse(
                dimension,
                self.halton_index,
                mix_bits(1 + ((dimension as u64) << 4)) as u32,
            )
        }
    }

    pub fn multiplicative_inverse(a: i64, n: i64) -> u64 {
        let (x, _) = Self::extended_gcd(a as u64, n as u64);
        x.rem_euclid(n) as u64
    }

    pub fn extended_gcd(a: u64, b: u64) -> (i64, i64) {
        if b == 0 {
            return (1, 0);
        }

        let (xp, yp) = Self::extended_gcd(b, a % b);

        let d = (a / b) as i64;

        (yp, xp - d * yp)
    }
}

impl SamplerTrait for HaltonSampler {
    fn samples_per_pixel(&self) -> i32 {
        self.samples_per_pixel
    }

    fn start_pixel_sample(&mut self, p: Point2i, sample_index: i32, dim: Option<u32>) {
        self.halton_index = 0;

        let sample_stride = self.base_scales[0] * self.base_scales[1];
        if sample_stride > 1 {
            let pm_x = p.x().rem_euclid(MAX_HALTON_RESOLUTION) as u64;
            let pm_y = p.y().rem_euclid(MAX_HALTON_RESOLUTION) as u64;

            let dim_offset_x = inverse_radical_inverse(pm_x, 2, self.base_exponents[0]);

            self.halton_index = self.halton_index.wrapping_add(
                dim_offset_x
                    .wrapping_mul(sample_stride / self.base_scales[0])
                    .wrapping_mul(self.mult_inverse[0]),
            );

            let dim_offset_y = inverse_radical_inverse(pm_y, 3, self.base_exponents[1]);

            self.halton_index = self.halton_index.wrapping_add(
                dim_offset_y
                    .wrapping_mul(sample_stride / self.base_scales[1])
                    .wrapping_mul(self.mult_inverse[1]),
            );

            self.halton_index %= sample_stride;
        }

        self.halton_index = self
            .halton_index
            .wrapping_add((sample_index as u64).wrapping_mul(sample_stride));

        self.dim = 2.max(dim.unwrap_or(0));
    }

    fn get1d(&mut self) -> Float {
        if self.dim > PRIME_TABLE_SIZE as u32 {
            self.dim = 2;
        }
        self.sample_dimension(self.dim)
    }

    fn get2d(&mut self) -> Point2f {
        if self.dim > PRIME_TABLE_SIZE as u32 {
            self.dim = 2;
        }
        let dim = self.dim;
        self.dim += 2;
        Point2f::new(self.sample_dimension(dim), self.sample_dimension(dim + 1))
    }

    fn get_pixel2d(&mut self) -> Point2f {
        Point2f::new(
            radical_inverse(0, self.halton_index >> self.base_exponents[0]),
            radical_inverse(1, self.halton_index >> self.base_exponents[1]),
        )
    }
}

#[repr(C)]
#[derive(Default, Debug, Clone, Copy)]
pub struct StratifiedSampler {
    x_pixel_samples: i32,
    y_pixel_samples: i32,
    jitter: bool,
    seed: u64,
    rng: Rng,
    pixel: Point2i,
    sample_index: i32,
    dim: u32,
}

impl StratifiedSampler {
    pub fn new(
        x_pixel_samples: i32,
        y_pixel_samples: i32,
        seed: Option<u64>,
        jitter: bool,
    ) -> Self {
        Self {
            x_pixel_samples,
            y_pixel_samples,
            jitter,
            seed: seed.unwrap_or(0),
            rng: Rng::default(),
            pixel: Point2i::default(),
            sample_index: 0,
            dim: 0,
        }
    }
}

impl SamplerTrait for StratifiedSampler {
    fn samples_per_pixel(&self) -> i32 {
        self.x_pixel_samples * self.y_pixel_samples
    }

    fn start_pixel_sample(&mut self, p: Point2i, sample_index: i32, dim: Option<u32>) {
        self.pixel = p;
        self.sample_index = sample_index;
        let hash_input = [p.x() as u64, p.y() as u64, self.seed];
        let sequence_index = hash_buffer(&hash_input, 0);
        self.rng.set_sequence(sequence_index);
        self.rng
            .advance((sample_index as u64) * 65536 + (dim.unwrap_or(0) as u64));
    }

    fn get1d(&mut self) -> Float {
        let hash_input = [
            self.pixel.x() as u64,
            self.pixel.y() as u64,
            self.dim as u64,
            self.seed,
        ];
        let hash = hash_buffer(&hash_input, 0);
        let stratum = permutation_element(
            self.sample_index as u32,
            self.samples_per_pixel() as u32,
            hash as u32,
        );
        self.dim += 1;

        let delta = if self.jitter {
            self.rng.uniform::<Float>()
        } else {
            0.5
        };
        (stratum as Float + delta) / (self.samples_per_pixel() as Float)
    }

    fn get2d(&mut self) -> Point2f {
        let hash_input = [
            self.pixel.x() as u64,
            self.pixel.y() as u64,
            self.dim as u64,
            self.seed,
        ];
        let hash = hash_buffer(&hash_input, 0);
        let stratum = permutation_element(
            self.sample_index as u32,
            self.samples_per_pixel() as u32,
            hash as u32,
        );
        self.dim += 2;
        let x = stratum % self.x_pixel_samples as u32;
        let y = stratum / self.y_pixel_samples as u32;
        let dx = if self.jitter {
            self.rng.uniform::<Float>()
        } else {
            0.5
        };
        let dy = if self.jitter {
            self.rng.uniform::<Float>()
        } else {
            0.5
        };
        Point2f::new(
            (x as Float + dx) / self.x_pixel_samples as Float,
            (y as Float + dy) / self.y_pixel_samples as Float,
        )
    }

    fn get_pixel2d(&mut self) -> Point2f {
        self.get2d()
    }
}

#[repr(C)]
#[derive(Default, Debug, Clone, Copy)]
pub struct PaddedSobolSampler {
    samples_per_pixel: i32,
    seed: u64,
    randomize: RandomizeStrategy,
    pixel: Point2i,
    sample_index: i32,
    dim: u32,
}

impl PaddedSobolSampler {
    pub fn new(samples_per_pixel: i32, randomize: RandomizeStrategy, seed: Option<u64>) -> Self {
        Self {
            samples_per_pixel,
            seed: seed.unwrap_or(0),
            randomize,
            pixel: Point2i::default(),
            sample_index: 0,
            dim: 0,
        }
    }

    fn sample_dimension(&self, dimension: u32, a: u32, hash: u32) -> Float {
        if self.randomize == RandomizeStrategy::None {
            return sobol_sample(a as u64, dimension, NoRandomizer);
        }
        match self.randomize {
            RandomizeStrategy::PermuteDigits => {
                sobol_sample(a as u64, dimension, BinaryPermuteScrambler::new(hash))
            }
            RandomizeStrategy::FastOwen => {
                sobol_sample(a as u64, dimension, FastOwenScrambler::new(hash))
            }
            RandomizeStrategy::Owen => sobol_sample(a as u64, dimension, OwenScrambler::new(hash)),
            RandomizeStrategy::None => unreachable!(),
        }
    }
}

impl SamplerTrait for PaddedSobolSampler {
    fn samples_per_pixel(&self) -> i32 {
        self.samples_per_pixel
    }
    fn start_pixel_sample(&mut self, p: Point2i, sample_index: i32, dim: Option<u32>) {
        self.pixel = p;
        self.sample_index = sample_index;
        self.dim = dim.unwrap_or(0);
    }

    fn get1d(&mut self) -> Float {
        let hash_input = [
            self.pixel.x() as u64,
            self.pixel.y() as u64,
            self.dim as u64,
            self.seed,
        ];
        let hash = hash_buffer(&hash_input, 0);
        let index = permutation_element(
            self.sample_index as u32,
            self.samples_per_pixel as u32,
            hash as u32,
        );
        self.sample_dimension(0, index, (hash >> 32) as u32)
    }
    fn get2d(&mut self) -> Point2f {
        let hash_input = [
            self.pixel.x() as u64,
            self.pixel.y() as u64,
            self.dim as u64,
            self.seed,
        ];
        let hash = hash_buffer(&hash_input, 0);
        let index = permutation_element(
            self.sample_index as u32,
            self.samples_per_pixel as u32,
            hash as u32,
        );
        self.dim += 2;
        Point2f::new(
            self.sample_dimension(0, index, hash as u32),
            self.sample_dimension(1, index, (hash >> 32) as u32),
        )
    }

    fn get_pixel2d(&mut self) -> Point2f {
        self.get2d()
    }
}

#[derive(Default, Debug, Clone)]
pub struct SobolSampler {
    samples_per_pixel: i32,
    scale: i32,
    seed: u64,
    randomize: RandomizeStrategy,
    pixel: Point2i,
    dim: u32,
    sobol_index: u64,
}

impl SobolSampler {
    pub fn new(
        samples_per_pixel: i32,
        full_resolution: Point2i,
        randomize: RandomizeStrategy,
        seed: Option<u64>,
    ) -> Self {
        let scale = round_up_pow2(full_resolution.x().max(full_resolution.y()));
        Self {
            samples_per_pixel,
            scale,
            seed: seed.unwrap_or(0),
            randomize,
            pixel: Point2i::default(),
            dim: 0,
            sobol_index: 0,
        }
    }

    fn sample_dimension(&self, dimension: u32) -> Float {
        if self.randomize == RandomizeStrategy::None {
            return sobol_sample(self.sobol_index, dimension, NoRandomizer);
        }
        let hash_input = [self.pixel.x() as u64, self.pixel.y() as u64, self.seed];
        let hash = hash_buffer(&hash_input, 0) as u32;
        match self.randomize {
            RandomizeStrategy::PermuteDigits => sobol_sample(
                self.sobol_index,
                dimension,
                BinaryPermuteScrambler::new(hash),
            ),
            RandomizeStrategy::FastOwen => {
                sobol_sample(self.sobol_index, dimension, FastOwenScrambler::new(hash))
            }
            RandomizeStrategy::Owen => {
                sobol_sample(self.sobol_index, dimension, OwenScrambler::new(hash))
            }
            RandomizeStrategy::None => unreachable!(),
        }
    }
}

impl SamplerTrait for SobolSampler {
    fn samples_per_pixel(&self) -> i32 {
        self.samples_per_pixel
    }
    fn start_pixel_sample(&mut self, p: Point2i, sample_index: i32, dim: Option<u32>) {
        self.pixel = p;
        self.dim = 2.max(dim.unwrap_or(0));
        self.sobol_index =
            sobol_interval_to_index(log2_int(self.scale as Float) as u32, sample_index as u64, p)
    }

    fn get1d(&mut self) -> Float {
        if self.dim >= N_SOBOL_DIMENSIONS as u32 {
            self.dim = 2;
        }

        let dim = self.dim;
        self.dim += 1;
        self.sample_dimension(dim)
    }

    fn get2d(&mut self) -> Point2f {
        if self.dim >= N_SOBOL_DIMENSIONS as u32 {
            self.dim = 2;
        }
        let u = Point2f::new(
            self.sample_dimension(self.dim),
            self.sample_dimension(self.dim + 1),
        );
        self.dim += 2;
        u
    }

    fn get_pixel2d(&mut self) -> Point2f {
        let mut u = Point2f::new(
            sobol_sample(self.sobol_index, 0, NoRandomizer),
            sobol_sample(self.sobol_index, 1, NoRandomizer),
        );
        u[0] = clamp(
            u[0] * self.scale as Float - self.pixel[0] as Float,
            0.,
            ONE_MINUS_EPSILON,
        ) as Float;
        u[1] = clamp(
            u[1] * self.scale as Float - self.pixel[1] as Float,
            1.,
            ONE_MINUS_EPSILON,
        ) as Float;
        u
    }
}

#[repr(C)]
#[derive(Default, Copy, Debug, Clone)]
pub struct ZSobolSampler {
    randomize: RandomizeStrategy,
    seed: u64,
    log2_samples_per_pixel: i32,
    n_base4_digits: u32,
    morton_index: u64,
    dim: u32,
}

impl ZSobolSampler {
    pub fn new(
        samples_per_pixel: i32,
        full_resolution: Point2i,
        randomize: RandomizeStrategy,
        seed: Option<u64>,
    ) -> Self {
        let log2_samples_per_pixel = log2_int(samples_per_pixel as Float) as u32;
        let res = round_up_pow2(full_resolution.x().max(full_resolution.y()));
        let log4_samples_per_pixel = log2_samples_per_pixel.div_ceil(2);
        let n_base4_digits = log2_int(res as Float) as u32 + log4_samples_per_pixel as u32;
        Self {
            randomize,
            seed: seed.unwrap_or(0),
            log2_samples_per_pixel: log2_samples_per_pixel as i32,
            n_base4_digits,
            morton_index: 0,
            dim: 0,
        }
    }

    fn get_sample_index(&self) -> u64 {
        const PERMUTATIONS: [[u8; 4]; 24] = [
            [0, 1, 2, 3],
            [0, 1, 3, 2],
            [0, 2, 1, 3],
            [0, 2, 3, 1],
            [0, 3, 2, 1],
            [0, 3, 1, 2],
            [1, 0, 2, 3],
            [1, 0, 3, 2],
            [1, 2, 0, 3],
            [1, 2, 3, 0],
            [1, 3, 2, 0],
            [1, 3, 0, 2],
            [2, 1, 0, 3],
            [2, 1, 3, 0],
            [2, 0, 1, 3],
            [2, 0, 3, 1],
            [2, 3, 0, 1],
            [2, 3, 1, 0],
            [3, 1, 2, 0],
            [3, 1, 0, 2],
            [3, 2, 1, 0],
            [3, 2, 0, 1],
            [3, 0, 2, 1],
            [3, 0, 1, 2],
        ];

        let mut sample_index = 0;
        let pow2_samples = (self.log2_samples_per_pixel & 1) != 0;
        let last_digit = if pow2_samples { 1 } else { 0 };

        for i in (last_digit..self.n_base4_digits).rev() {
            let digit_shift = (2 * i) - if pow2_samples { 1 } else { 0 };

            let mut digit = (self.morton_index >> digit_shift) & 3;

            let higher_digits = self.morton_index >> (digit_shift + 2);

            let mix_input = higher_digits ^ (0x55555555 * self.dim as u64);
            let p = (mix_bits(mix_input) >> 24) % 24;

            digit = PERMUTATIONS[p as usize][digit as usize] as u64;

            sample_index |= digit << digit_shift;
        }

        if pow2_samples {
            let lsb = self.morton_index & 1;
            sample_index |= lsb;
        }

        sample_index
    }
}

impl SamplerTrait for ZSobolSampler {
    fn samples_per_pixel(&self) -> i32 {
        todo!()
    }
    fn start_pixel_sample(&mut self, p: Point2i, sample_index: i32, dim: Option<u32>) {
        self.dim = dim.unwrap_or(0);
        self.morton_index = (encode_morton_2(p.x() as u32, p.y() as u32)
            << self.log2_samples_per_pixel)
            | sample_index as u64
    }

    fn get1d(&mut self) -> Float {
        let sample_index = self.get_sample_index();
        let hash_input = [self.dim as u64, self.seed];
        let hash = hash_buffer(&hash_input, 0) as u32;
        self.dim += 1;
        if self.randomize == RandomizeStrategy::None {
            return sobol_sample(sample_index, self.dim, NoRandomizer);
        }
        match self.randomize {
            RandomizeStrategy::PermuteDigits => {
                sobol_sample(sample_index, self.dim, BinaryPermuteScrambler::new(hash))
            }
            RandomizeStrategy::FastOwen => {
                sobol_sample(sample_index, self.dim, FastOwenScrambler::new(hash))
            }
            RandomizeStrategy::Owen => {
                sobol_sample(sample_index, self.dim, OwenScrambler::new(hash))
            }
            RandomizeStrategy::None => unreachable!(),
        }
    }

    fn get2d(&mut self) -> Point2f {
        let sample_index = self.get_sample_index();
        self.dim += 2;
        let hash_input = [self.dim as u64, self.seed];
        let hash = hash_buffer(&hash_input, 0);
        let sample_hash = [hash as u32, (hash >> 32) as u32];
        if self.randomize == RandomizeStrategy::None {
            return Point2f::new(
                sobol_sample(sample_index, 0, NoRandomizer),
                sobol_sample(sample_index, 1, NoRandomizer),
            );
        }
        match self.randomize {
            RandomizeStrategy::PermuteDigits => Point2f::new(
                sobol_sample(sample_index, 0, BinaryPermuteScrambler::new(sample_hash[0])),
                sobol_sample(sample_index, 1, BinaryPermuteScrambler::new(sample_hash[1])),
            ),
            RandomizeStrategy::FastOwen => Point2f::new(
                sobol_sample(sample_index, 0, FastOwenScrambler::new(sample_hash[0])),
                sobol_sample(sample_index, 1, FastOwenScrambler::new(sample_hash[1])),
            ),
            RandomizeStrategy::Owen => Point2f::new(
                sobol_sample(sample_index, 0, OwenScrambler::new(sample_hash[0])),
                sobol_sample(sample_index, 1, OwenScrambler::new(sample_hash[1])),
            ),
            RandomizeStrategy::None => unreachable!(),
        }
    }

    fn get_pixel2d(&mut self) -> Point2f {
        self.get2d()
    }
}

#[derive(Default, Debug, Clone)]
pub struct MLTSampler;
impl SamplerTrait for MLTSampler {
    fn samples_per_pixel(&self) -> i32 {
        todo!()
    }
    fn start_pixel_sample(&mut self, _p: Point2i, _sample_index: i32, _dim: Option<u32>) {
        todo!()
    }
    fn get1d(&mut self) -> Float {
        todo!()
    }
    fn get2d(&mut self) -> Point2f {
        todo!()
    }
    fn get_pixel2d(&mut self) -> Point2f {
        todo!()
    }
}

#[enum_dispatch]
pub trait SamplerTrait {
    fn samples_per_pixel(&self) -> i32;
    fn start_pixel_sample(&mut self, p: Point2i, sample_index: i32, dim: Option<u32>);
    fn get1d(&mut self) -> Float;
    fn get2d(&mut self) -> Point2f;
    fn get_pixel2d(&mut self) -> Point2f;
}

#[enum_dispatch(SamplerTrait)]
#[derive(Debug, Clone)]
pub enum Sampler {
    Independent(IndependentSampler),
    Stratified(StratifiedSampler),
    Halton(HaltonSampler),
    PaddedSobol(PaddedSobolSampler),
    Sobol(SobolSampler),
    ZSobol(ZSobolSampler),
    MLT(MLTSampler),
}
