use bevy::{
    prelude::{shape::Icosphere, *},
    render::mesh::Indices,
};
use bevy_rapier3d::prelude::*;
use hexasphere::shapes::IcoSphere;
use noise::{HybridMulti, NoiseFn, SuperSimplex};
use wgpu::PrimitiveTopology;

use crate::Label;

pub const PLANET_RADIUS: f32 = 6000.0;

#[derive(Component)]
pub struct CyberPlanet;

fn spawn_planet(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    let color = Color::rgb(0.2, 0.1, 0.2);
    let isphere = Icosphere {
        radius: PLANET_RADIUS,
        subdivisions: 88,
    };

    let (mesh, shape) = gen_planet(isphere);

    let pbody = (RigidBody::Fixed, Ccd { enabled: true });

    let pcollide = (
        shape,
        Friction {
            coefficient: 0.8,
            ..Default::default()
        },
        Restitution::new(0.8),
    );

    commands
        .spawn(PbrBundle {
            mesh: meshes.add(mesh),
            material: materials.add(StandardMaterial {
                base_color: color,
                metallic: 0.1,
                perceptual_roughness: 0.3,
                alpha_mode: AlphaMode::Opaque,
                ..Default::default()
            }),

            ..Default::default()
        })
        .insert(pbody)
        .insert(pcollide)
        .insert(CyberPlanet);
}

pub struct CyberPlanetPlugin;
impl Plugin for CyberPlanetPlugin {
    fn build(&self, app: &mut App) {
        app.add_startup_system(spawn_planet.label(Label::Geometry));
    }
}

//---------------------------------------------------------------------
// utils
//---------------------------------------------------------------------

fn gen_planet(sphere: Icosphere) -> (Mesh, Collider) {
    // straight-up stolen from Bevy's impl of Mesh from Icosphere, so I can do the
    // displacement before normals are calculated.
    let generated = IcoSphere::new(sphere.subdivisions, |point| {
        let inclination = point.y.acos();
        let azimuth = point.z.atan2(point.x);

        let norm_inclination = inclination / std::f32::consts::PI;
        let norm_azimuth = 0.5 - (azimuth / std::f32::consts::TAU);

        [norm_azimuth, norm_inclination]
    });

    let noise = HybridMulti::<SuperSimplex>::default();

    let noisy_points = generated
        .raw_points()
        .iter()
        .map(|&p| {
            let disp = noise.get(p.as_dvec3().into()) as f32 * 0.04;
            let pt = p; // + (p.normalize() * disp);
            pt.into()
        })
        .collect::<Vec<[f32; 3]>>();

    let points = noisy_points
        .iter()
        .map(|&p| (Vec3::from_slice(&p) * sphere.radius).into())
        .collect::<Vec<[f32; 3]>>();

    let uvs = generated.raw_data().to_owned();

    let mut indices = Vec::with_capacity(generated.indices_per_main_triangle() * 20);

    for i in 0..20 {
        generated.get_indices(i, &mut indices);
    }

    let mut idxs = Vec::new();
    for idx in indices.chunks_exact(3) {
        idxs.push([idx[0], idx[1], idx[2]]);
    }

    let shape = Collider::trimesh(points.iter().map(|p| Vect::from_slice(p)).collect(), idxs);

    let indices = Indices::U32(indices);

    let mut mesh = Mesh::new(PrimitiveTopology::TriangleList);
    mesh.set_indices(Some(indices));
    mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, points);
    mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs);
    (mesh, shape)
}
