use sge_vectors::{Mat3, Vec3};
use sge_color::Color;
use sge_types::MaterialVertex3D;

use crate::{
    api::create_flat_material,
    object_3d::{Mesh, MeshRef, Object3D, Object3DRef},
};

pub trait HasBounds3D {
    fn bounds(&self) -> AABB3D;
}

pub trait Shape3D: HasBounds3D {
    fn create_mesh(&self) -> Mesh;
}

#[derive(Debug, Clone, Copy)]
pub struct AABB3D {
    pub min: Vec3,
    pub max: Vec3,
}

impl AABB3D {
    pub fn new(min: Vec3, max: Vec3) -> Self {
        Self { min, max }
    }

    pub fn from_center_size(center: Vec3, size: Vec3) -> Self {
        let half_size = size * 0.5;
        Self {
            min: center - half_size,
            max: center + half_size,
        }
    }

    pub fn intersects(&self, other: &Self) -> bool {
        self.min.x <= other.max.x
            && self.max.x >= other.min.x
            && self.min.y <= other.max.y
            && self.max.y >= other.min.y
            && self.min.z <= other.max.z
            && self.max.z >= other.min.z
    }

    pub fn expand(self, amount: f32) -> Self {
        Self {
            min: self.min - Vec3::splat(amount),
            max: self.max + Vec3::splat(amount),
        }
    }
}

pub struct Line3D {
    pub start: Vec3,
    pub end: Vec3,
    pub thickness: f32,
}

impl Line3D {
    pub fn new(start: Vec3, end: Vec3, thickness: f32) -> Self {
        Self {
            start,
            end,
            thickness,
        }
    }

    pub fn as_cuboid(&self) -> Cuboid {
        Cuboid::from_segment(self.start, self.end, self.thickness)
    }
}

impl Shape3D for Line3D {
    fn create_mesh(&self) -> Mesh {
        self.as_cuboid().create_mesh()
    }
}

impl HasBounds3D for Line3D {
    fn bounds(&self) -> AABB3D {
        self.as_cuboid().bounds()
    }
}

pub fn line_3d(start: Vec3, end: Vec3, thickness: f32) -> MeshRef {
    let line = Line3D::new(start, end, thickness);
    line.create_mesh().create()
}

pub fn line_3d_flat(start: Vec3, end: Vec3, thickness: f32, color: Color) -> Object3DRef {
    let mesh = line_3d(start, end, thickness);
    let material = create_flat_material(color);
    Object3D::from_mesh_and_material(mesh, material)
}

pub fn cube_wireframe(center: Vec3, size: f32, thickness: f32) -> MeshRef {
    let half = size / 2.0;

    let v0 = center + Vec3::new(-half, -half, -half);
    let v1 = center + Vec3::new(half, -half, -half);
    let v2 = center + Vec3::new(half, -half, half);
    let v3 = center + Vec3::new(-half, -half, half);

    let v4 = center + Vec3::new(-half, half, -half);
    let v5 = center + Vec3::new(half, half, -half);
    let v6 = center + Vec3::new(half, half, half);
    let v7 = center + Vec3::new(-half, half, half);

    let edges = [
        (v0, v1),
        (v1, v2),
        (v2, v3),
        (v3, v0),
        (v4, v5),
        (v5, v6),
        (v6, v7),
        (v7, v4),
        (v0, v4),
        (v1, v5),
        (v2, v6),
        (v3, v7),
    ];

    let mut vertices = Vec::<MaterialVertex3D>::new();
    let mut indices = Vec::<u32>::new();

    const CUBOID_INDICES: &[u32] = &[
        0, 1, 2, 2, 1, 3, 4, 6, 5, 5, 6, 7, 0, 2, 4, 4, 2, 6, 1, 5, 3, 3, 5, 7, 0, 4, 1, 1, 4, 5,
        2, 3, 6, 6, 3, 7,
    ];

    for (p1_raw, p2_raw) in edges {
        let dir = (p2_raw - p1_raw).normalize();

        let p1 = p1_raw - dir * (thickness * 0.5);
        let p2 = p2_raw + dir * (thickness * 0.5);

        let cuboid = Cuboid::from_segment(p1, p2, thickness);

        let verts = cuboid.vertices();
        let base = vertices.len() as u32;

        for v in verts {
            vertices.push(MaterialVertex3D::from_pos(v));
        }

        for &i in CUBOID_INDICES {
            indices.push(base + i);
        }
    }

    Mesh::from_points(vertices, indices).unwrap()
}

pub fn cube_wireframe_flat(center: Vec3, size: f32, thickness: f32, color: Color) -> Object3DRef {
    let mesh = cube_wireframe(center, size, thickness);
    let material = create_flat_material(color);
    Object3D::from_mesh_and_material(mesh, material)
}

pub fn cuboid(center: Vec3, size: Vec3) -> MeshRef {
    Cuboid::new(center, size).create_mesh().create()
}

pub fn cube(center: Vec3, size: f32) -> MeshRef {
    cuboid(center, Vec3::splat(size))
}

pub fn cuboid_with_orientation(center: Vec3, size: Vec3, orientation: Mat3) -> MeshRef {
    Cuboid::new_with_orientation(center, size, orientation)
        .create_mesh()
        .create()
}

pub fn cube_with_orientation(center: Vec3, size: f32, orientation: Mat3) -> MeshRef {
    cuboid_with_orientation(center, Vec3::splat(size), orientation)
}

pub fn cuboid_from_extents(center: Vec3, extents: Vec3) -> MeshRef {
    Cuboid {
        center,
        extents,
        orientation: Mat3::IDENTITY,
    }
    .create_mesh()
    .create()
}

pub fn cuboid_from_extents_with_orientation(
    center: Vec3,
    extents: Vec3,
    orientation: Mat3,
) -> MeshRef {
    Cuboid {
        center,
        extents,
        orientation,
    }
    .create_mesh()
    .create()
}

#[derive(Debug, Clone, Copy)]
pub struct Cuboid {
    pub center: Vec3,
    pub extents: Vec3,
    pub orientation: Mat3,
}

impl Cuboid {
    pub fn new(center: Vec3, size: Vec3) -> Self {
        Self {
            center,
            extents: size * 0.5,
            orientation: Mat3::IDENTITY,
        }
    }

    pub fn new_with_orientation(center: Vec3, size: Vec3, orientation: Mat3) -> Self {
        Self {
            center,
            extents: size * 0.5,
            orientation,
        }
    }

    pub fn from_negative_corner(negative_corner: Vec3, size: Vec3) -> Self {
        Self {
            center: negative_corner + size * 0.5,
            extents: size * 0.5,
            orientation: Mat3::IDENTITY,
        }
    }

    pub fn from_negative_corner_with_orientation(
        negative_corner: Vec3,
        size: Vec3,
        orientation: Mat3,
    ) -> Self {
        Self {
            center: negative_corner + size * 0.5,
            extents: size * 0.5,
            orientation,
        }
    }

    pub fn from_segment(p1: Vec3, p2: Vec3, thickness: f32) -> Self {
        let dir = p2 - p1;
        let len = dir.length();
        let x_axis = dir.normalize();

        let ref_vec = if x_axis.y.abs() < 0.9 {
            Vec3::Y
        } else {
            Vec3::X
        };

        let z_axis = x_axis.cross(ref_vec).normalize();
        let y_axis = z_axis.cross(x_axis).normalize();
        let orientation = Mat3::from_cols(x_axis, y_axis, z_axis);

        Self {
            center: (p1 + p2) * 0.5,
            extents: Vec3::new(len * 0.5, thickness * 0.5, thickness * 0.5),
            orientation,
        }
    }

    fn vertices(&self) -> [Vec3; 8] {
        let ex = self.orientation * Vec3::new(self.extents.x, 0.0, 0.0);
        let ey = self.orientation * Vec3::new(0.0, self.extents.y, 0.0);
        let ez = self.orientation * Vec3::new(0.0, 0.0, self.extents.z);

        [
            self.center + ex + ey + ez,
            self.center + ex + ey - ez,
            self.center + ex - ey + ez,
            self.center + ex - ey - ez,
            self.center - ex + ey + ez,
            self.center - ex + ey - ez,
            self.center - ex - ey + ez,
            self.center - ex - ey - ez,
        ]
    }
}

impl Shape3D for Cuboid {
    fn create_mesh(&self) -> Mesh {
        let v = self.vertices();

        let vertices: Vec<MaterialVertex3D> =
            v.iter().map(|&p| MaterialVertex3D::from_pos(p)).collect();

        const INDICES: &[u32] = &[
            0, 1, 2, 2, 1, 3, 4, 6, 5, 5, 6, 7, 0, 2, 4, 4, 2, 6, 1, 5, 3, 3, 5, 7, 0, 4, 1, 1, 4,
            5, 2, 3, 6, 6, 3, 7,
        ];

        Mesh::from_points_internal(vertices, INDICES.to_vec()).unwrap()
    }
}

impl HasBounds3D for Cuboid {
    fn bounds(&self) -> AABB3D {
        let verts = self.vertices();
        let mut min = verts[0];
        let mut max = verts[0];

        for &v in &verts {
            min = min.min(v);
            max = max.max(v);
        }

        AABB3D::new(min, max)
    }
}
