use std::collections::HashMap;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LodLevel {
    Low,
    Medium,
    High,
}

#[derive(Debug, Clone, Copy)]
pub struct Vertex {
    pub position: [f32; 3],
    pub normal: [f32; 3],
    pub uv: [f32; 2],
}

#[derive(Debug, Clone)]
pub struct MeshData {
    pub vertices: Vec<Vertex>,
    pub indices: Vec<u32>,
}

impl MeshData {
    pub fn vertex_count(&self) -> usize {
        self.vertices.len()
    }

    pub fn triangle_count(&self) -> usize {
        self.indices.len() / 3
    }

    pub fn positions_flat(&self) -> Vec<f32> {
        self.vertices.iter().flat_map(|v| v.position).collect()
    }

    pub fn normals_flat(&self) -> Vec<f32> {
        self.vertices.iter().flat_map(|v| v.normal).collect()
    }

    pub fn uvs_flat(&self) -> Vec<f32> {
        self.vertices.iter().flat_map(|v| v.uv).collect()
    }
}

fn subdivisions_for_lod(lod: LodLevel) -> u32 {
    match lod {
        LodLevel::Low => 2,
        LodLevel::Medium => 4,
        LodLevel::High => 6,
    }
}

fn midpoint_index(
    vertices: &mut Vec<[f32; 3]>,
    cache: &mut HashMap<u64, u32>,
    i1: u32,
    i2: u32,
) -> u32 {
    let key = if i1 < i2 {
        (i1 as u64) << 32 | i2 as u64
    } else {
        (i2 as u64) << 32 | i1 as u64
    };
    if let Some(&idx) = cache.get(&key) {
        return idx;
    }
    let a = vertices[i1 as usize];
    let b = vertices[i2 as usize];
    let mid = [
        (a[0] + b[0]) * 0.5,
        (a[1] + b[1]) * 0.5,
        (a[2] + b[2]) * 0.5,
    ];
    let len = (mid[0] * mid[0] + mid[1] * mid[1] + mid[2] * mid[2]).sqrt();
    let normalized = [mid[0] / len, mid[1] / len, mid[2] / len];
    let idx = vertices.len() as u32;
    vertices.push(normalized);
    cache.insert(key, idx);
    idx
}

fn build_icosphere(subdivisions: u32) -> (Vec<[f32; 3]>, Vec<u32>) {
    let t = (1.0 + 5.0_f32.sqrt()) / 2.0;

    let mut vertices: Vec<[f32; 3]> = vec![
        [-1.0, t, 0.0],
        [1.0, t, 0.0],
        [-1.0, -t, 0.0],
        [1.0, -t, 0.0],
        [0.0, -1.0, t],
        [0.0, 1.0, t],
        [0.0, -1.0, -t],
        [0.0, 1.0, -t],
        [t, 0.0, -1.0],
        [t, 0.0, 1.0],
        [-t, 0.0, -1.0],
        [-t, 0.0, 1.0],
    ];

    for v in &mut vertices {
        let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
        v[0] /= len;
        v[1] /= len;
        v[2] /= len;
    }

    let mut indices: Vec<u32> = vec![
        0, 11, 5, 0, 5, 1, 0, 1, 7, 0, 7, 10, 0, 10, 11, 1, 5, 9, 5, 11, 4, 11, 10, 2, 10, 7, 6, 7,
        1, 8, 3, 9, 4, 3, 4, 2, 3, 2, 6, 3, 6, 8, 3, 8, 9, 4, 9, 5, 2, 4, 11, 6, 2, 10, 8, 6, 7, 9,
        8, 1,
    ];

    for _ in 0..subdivisions {
        let mut cache: HashMap<u64, u32> = HashMap::new();
        let mut new_indices = Vec::with_capacity(indices.len() * 4);
        for tri in indices.chunks(3) {
            let a = tri[0];
            let b = tri[1];
            let c = tri[2];
            let ab = midpoint_index(&mut vertices, &mut cache, a, b);
            let bc = midpoint_index(&mut vertices, &mut cache, b, c);
            let ca = midpoint_index(&mut vertices, &mut cache, c, a);
            new_indices.extend_from_slice(&[a, ab, ca]);
            new_indices.extend_from_slice(&[b, bc, ab]);
            new_indices.extend_from_slice(&[c, ca, bc]);
            new_indices.extend_from_slice(&[ab, bc, ca]);
        }
        indices = new_indices;
    }
    (vertices, indices)
}

fn sphere_uv(normal: &[f32; 3]) -> [f32; 2] {
    let u = 0.5 + normal[2].atan2(normal[0]) / (2.0 * std::f32::consts::PI);
    let v = 0.5 - normal[1].asin() / std::f32::consts::PI;
    [u, v]
}

pub fn generate_sphere(radius: f32, lod: LodLevel) -> MeshData {
    let subdivisions = subdivisions_for_lod(lod);
    generate_sphere_subdivisions(radius, subdivisions)
}

pub fn generate_sphere_subdivisions(radius: f32, subdivisions: u32) -> MeshData {
    let (unit_verts, indices) = build_icosphere(subdivisions);
    let vertices = unit_verts
        .iter()
        .map(|n| Vertex {
            position: [n[0] * radius, n[1] * radius, n[2] * radius],
            normal: *n,
            uv: sphere_uv(n),
        })
        .collect();
    MeshData { vertices, indices }
}

pub fn generate_oblate_sphere(
    equatorial_radius: f32,
    polar_radius: f32,
    lod: LodLevel,
) -> MeshData {
    let subdivisions = subdivisions_for_lod(lod);
    generate_oblate_sphere_subdivisions(equatorial_radius, polar_radius, subdivisions)
}

pub fn generate_oblate_sphere_subdivisions(
    equatorial_radius: f32,
    polar_radius: f32,
    subdivisions: u32,
) -> MeshData {
    let (unit_verts, indices) = build_icosphere(subdivisions);
    let eq2 = equatorial_radius * equatorial_radius;
    let po2 = polar_radius * polar_radius;
    let vertices = unit_verts
        .iter()
        .map(|n| {
            let px = n[0] * equatorial_radius;
            let py = n[1] * polar_radius;
            let pz = n[2] * equatorial_radius;
            let nx = n[0] / eq2;
            let ny = n[1] / po2;
            let nz = n[2] / eq2;
            let nlen = (nx * nx + ny * ny + nz * nz).sqrt();
            Vertex {
                position: [px, py, pz],
                normal: [nx / nlen, ny / nlen, nz / nlen],
                uv: sphere_uv(n),
            }
        })
        .collect();
    MeshData { vertices, indices }
}

pub fn generate_ring(
    inner_radius: f32,
    outer_radius: f32,
    segments: u32,
    radial_segments: u32,
) -> MeshData {
    let mut vertices = Vec::new();
    let mut indices = Vec::new();

    for r in 0..=radial_segments {
        let t = r as f32 / radial_segments as f32;
        let radius = inner_radius + t * (outer_radius - inner_radius);
        for s in 0..=segments {
            let theta = s as f32 / segments as f32 * 2.0 * std::f32::consts::PI;
            let x = radius * theta.cos();
            let z = radius * theta.sin();
            vertices.push(Vertex {
                position: [x, 0.0, z],
                normal: [0.0, 1.0, 0.0],
                uv: [s as f32 / segments as f32, t],
            });
        }
    }

    let stride = segments + 1;
    for r in 0..radial_segments {
        for s in 0..segments {
            let i0 = r * stride + s;
            let i1 = i0 + 1;
            let i2 = i0 + stride;
            let i3 = i2 + 1;
            indices.extend_from_slice(&[i0, i2, i1, i1, i2, i3]);
        }
    }

    MeshData { vertices, indices }
}
