// There are many math functions in this file, so we allow single letter variable names.
#![allow(clippy::many_single_char_names)]
use log::trace;
use rand::seq::SliceRandom;
use rand::Rng;

use crate::noise::NoiseSource;
use crate::vec3::dot;
use crate::vec3::Vec3;

pub struct Perlin {
    ran_vec: Vec<Vec3>,
    perm_x: Vec<usize>,
    perm_y: Vec<usize>,
    perm_z: Vec<usize>,
}

fn perlin_generate<R>(rng: &mut R) -> Vec<Vec3>
where
    R: Rng,
{
    (0..256)
        .map(|_| {
            Vec3::new(
                rng.gen_range(-1. ..1.),
                rng.gen_range(-1. ..1.),
                rng.gen_range(-1. ..1.),
            )
            .unit_vector()
        })
        .collect()
}

fn perlin_generate_perm<R>(rng: &mut R) -> Vec<usize>
where
    R: Rng,
{
    let mut p: Vec<usize> = (0..256).map(|i| i).collect();
    p.shuffle(rng);
    p
}

fn perlin_interp(c: [[[Vec3; 2]; 2]; 2], u: f32, v: f32, w: f32) -> f32 {
    // Hermite cubic to round off interpolation and attempt to address 'mach bands'.
    let uu = u * u * (3. - 2. * u);
    let vv = v * v * (3. - 2. * v);
    let ww = w * w * (3. - 2. * w);

    let mut accum = 0.;
    for i in 0..2 {
        for j in 0..2 {
            for k in 0..2 {
                let weight_v = Vec3::new(u - i as f32, v - j as f32, w - k as f32);

                let i = i as f32;
                let j = j as f32;
                let k = k as f32;
                accum += (i * uu + (1. - i) * (1. - uu))
                    * (j * vv + (1. - j) * (1. - vv))
                    * (k * ww + (1. - k) * (1. - ww))
                    * dot(c[i as usize][j as usize][k as usize], weight_v);
            }
        }
    }
    //info!("u {} v {} accum {}", u, v, accum);
    accum
}

impl Perlin {
    pub fn new<R>(rng: &mut R) -> Perlin
    where
        R: Rng,
    {
        let p = Perlin {
            ran_vec: perlin_generate(rng),
            perm_x: perlin_generate_perm(rng),
            perm_y: perlin_generate_perm(rng),
            perm_z: perlin_generate_perm(rng),
        };
        trace!(target: "perlin",
            "ran_vec: {}",
            p.ran_vec
                .iter()
                .map(|v| v.to_string())
                .collect::<Vec<String>>()
                .join(", ")
        );
        trace!(target: "perlin",
            "perm_x: {}",
            p.perm_x
                .iter()
                .map(|v| v.to_string())
                .collect::<Vec<String>>()
                .join(", ")
        );
        trace!(target: "perlin",
            "perm_y: {}",
            p.perm_y
                .iter()
                .map(|v| v.to_string())
                .collect::<Vec<String>>()
                .join(", ")
        );
        trace!(target: "perlin",
            "perm_z: {}",
            p.perm_z
                .iter()
                .map(|v| v.to_string())
                .collect::<Vec<String>>()
                .join(", ")
        );
        p
    }

    pub fn turb(&self, p: Vec3, depth: usize) -> f32 {
        let mut accum = 0.;
        let mut temp_p = p;
        let mut weight = 1.;
        for _ in 0..depth {
            accum += weight * self.value(temp_p);
            weight *= 0.5;
            temp_p = temp_p * 0.2;
        }
        accum.abs()
    }
}

impl NoiseSource for Perlin {
    fn value(&self, p: Vec3) -> f32 {
        let u = p.x - p.x.floor();
        let v = p.y - p.y.floor();
        let w = p.z - p.z.floor();

        let i = p.x.floor() as usize;
        let j = p.y.floor() as usize;
        let k = p.z.floor() as usize;

        let mut c: [[[Vec3; 2]; 2]; 2] = Default::default();

        // Squelch warning about di only being used to index, as this way reads more consistently.
        #[allow(clippy::needless_range_loop)]
        for di in 0..2 {
            for dj in 0..2 {
                for dk in 0..2 {
                    c[di][dj][dk] = self.ran_vec[self.perm_x[(i + di) & 255]
                        ^ self.perm_y[(j + dj) & 255]
                        ^ self.perm_z[(k + dk) & 255]]
                }
            }
        }
        (1. + perlin_interp(c, u, v, w)) * 0.5
    }
}
