use crate::math::calc_triangle_surface_normal;
use crate::mesh::Mesh;
use crate::util::builder;
use crate::vector::{Vec2, Vec3};
use crate::vertex::{Builder as VertexBuilder, Vertex};

builder! {
/// Cube mesh creation specification.
#[builder(name = CreationSpecBuilder, derives = (Debug, Default))]
#[derive(Debug, Default)]
#[non_exhaustive]
pub struct CreationSpec
{
    pub width: f32,
    pub height: f32,
    pub depth: f32,
}
}

impl CreationSpec
{
    /// Returns a new `CreationSpec` builder.
    #[must_use]
    pub fn builder() -> CreationSpecBuilder
    {
        CreationSpecBuilder::default()
    }
}

/// Describes a single side of a cube (obviously).
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Side
{
    /// +Z
    Front,

    /// -Z
    Back,

    /// -X
    Left,

    /// +X
    Right,

    /// +Y
    Top,

    /// -Y
    Bottom,
}

/// Describes what location on a side of a cube a face is.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum FaceLocation
{
    /// 🮝
    RightUp,

    /// 🮟
    LeftDown,
}

/// Creates a cube mesh.
///
/// By default, the texture coordinates are arranged so that the full texture is visible
/// on every side. This can be changed inside of the `face_cb` function.
pub fn create(
    creation_spec: CreationSpec,
    face_cb: impl FnMut(FaceVertices, Side, FaceLocation) -> FaceVertices,
) -> Mesh
{
    let mut data = Data::default();

    create_side(&SidePositions::new_top(&creation_spec), &mut data);
    create_side(&SidePositions::new_bottom(&creation_spec), &mut data);
    create_side(&SidePositions::new_left(&creation_spec), &mut data);
    create_side(&SidePositions::new_right(&creation_spec), &mut data);
    create_side(&SidePositions::new_back(&creation_spec), &mut data);
    create_side(&SidePositions::new_front(&creation_spec), &mut data);

    data.into_mesh(face_cb)
}

#[derive(Debug, Default)]
struct Data
{
    faces: Vec<Face>,
    vertex_data: VertexData,
}

#[derive(Debug, Default)]
struct VertexData
{
    vertex_positions: Vec<Vec3<f32>>,
    vertex_normals: Vec<Vec3<f32>>,
}

impl Data
{
    fn into_mesh(
        self,
        mut face_cb: impl FnMut(FaceVertices, Side, FaceLocation) -> FaceVertices,
    ) -> Mesh
    {
        let mut vertices = Vec::<Vertex>::with_capacity(self.faces.len() * 3);
        let mut indices = Vec::<u32>::with_capacity(self.faces.len() * 3);

        let mut face_location = FaceLocation::RightUp;

        let Self { faces, vertex_data } = self;

        for face in faces {
            let side = face.side;

            let face_vertices = face_cb(
                FaceVertices::new(face, &vertex_data)
                    .with_full_per_side_tex_coords(face_location),
                side,
                face_location,
            );

            for vertex in face_vertices.vertices {
                if let Some((prev_vertex_index, _)) = vertices
                    .iter()
                    .enumerate()
                    .find(|(_, prev_vertex)| *prev_vertex == &vertex)
                {
                    indices
                        .push(u32::try_from(prev_vertex_index).expect(
                            "Vertex index does not fit into 32-bit unsigned int",
                        ));

                    continue;
                }

                vertices.push(vertex);

                let vertex_index = u32::try_from(vertices.len() - 1)
                    .expect("Vertex index does not fit into 32-bit unsigned int");

                indices.push(vertex_index);
            }

            match face_location {
                FaceLocation::RightUp => face_location = FaceLocation::LeftDown,
                FaceLocation::LeftDown => face_location = FaceLocation::RightUp,
            }
        }

        Mesh::new(vertices, Some(indices))
    }
}

/// The vertices of a single face of a cube.
#[derive(Debug, Default, Clone)]
pub struct FaceVertices
{
    /// The three vertices of a face in counter-clockwise order.
    ///
    /// Order when [`FaceLocation::RightUp`]:
    /// ```text
    /// ₂  ₁
    ///  🮝
    ///    ³
    /// ```
    ///
    /// Order when [`FaceLocation::LeftDown`]:
    /// ```text
    /// ₁
    ///  🮟
    /// ²  ³
    /// ```
    pub vertices: [Vertex; 3],
}

impl FaceVertices
{
    fn new(face: Face, vertex_data: &VertexData) -> Self
    {
        Self {
            vertices: face.vertices.map(|face_vertex| {
                let vertex_pos = vertex_data
                    .vertex_positions
                    .get(face_vertex.pos_index as usize)
                    .expect("Vertex position index is out of bounds")
                    .clone();

                let vertex_normal = vertex_data
                    .vertex_normals
                    .get(face_vertex.normal_index as usize)
                    .expect("Vertex normal index is out of bounds")
                    .clone();

                VertexBuilder::default()
                    .pos(vertex_pos)
                    .normal(vertex_normal)
                    .build()
            }),
        }
    }

    fn with_full_per_side_tex_coords(mut self, face_location: FaceLocation) -> Self
    {
        match face_location {
            FaceLocation::RightUp => {
                self.vertices[0].texture_coords = Vec2 { x: 1.0, y: 1.0 };
                self.vertices[1].texture_coords = Vec2 { x: 0.0, y: 1.0 };
                self.vertices[2].texture_coords = Vec2 { x: 1.0, y: 0.0 };
            }
            FaceLocation::LeftDown => {
                self.vertices[0].texture_coords = Vec2 { x: 0.0, y: 1.0 };
                self.vertices[1].texture_coords = Vec2 { x: 0.0, y: 0.0 };
                self.vertices[2].texture_coords = Vec2 { x: 1.0, y: 0.0 };
            }
        };

        self
    }
}

#[derive(Debug)]
struct Face
{
    vertices: [FaceVertex; 3],
    side: Side,
}

#[derive(Debug, PartialEq, Eq, Hash, Clone)]
struct FaceVertex
{
    pos_index: u32,
    normal_index: u32,
}

#[derive(Debug)]
struct SidePositions
{
    up_left: Vec3<f32>,
    up_right: Vec3<f32>,
    down_left: Vec3<f32>,
    down_right: Vec3<f32>,
    normal_calc_order: NormalCalcOrder,
    side: Side,
}

impl SidePositions
{
    fn new_top(creation_spec: &CreationSpec) -> Self
    {
        let up_left = Vec3 {
            x: -(creation_spec.width / 2.0),
            y: creation_spec.height / 2.0,
            z: creation_spec.depth / 2.0,
        };

        let down_right = Vec3 {
            x: creation_spec.width / 2.0,
            y: creation_spec.height / 2.0,
            z: -(creation_spec.depth / 2.0),
        };

        Self {
            up_left,
            up_right: Vec3 { x: down_right.x, ..up_left.clone() },
            down_left: Vec3 { x: up_left.x, ..down_right.clone() },
            down_right,
            normal_calc_order: NormalCalcOrder::Clockwise,
            side: Side::Top,
        }
    }

    fn new_bottom(creation_spec: &CreationSpec) -> Self
    {
        let up_left = Vec3 {
            x: -(creation_spec.width / 2.0),
            y: -creation_spec.height / 2.0,
            z: creation_spec.depth / 2.0,
        };

        let down_right = Vec3 {
            x: creation_spec.width / 2.0,
            y: -creation_spec.height / 2.0,
            z: -(creation_spec.depth / 2.0),
        };

        Self {
            up_left,
            up_right: Vec3 { x: down_right.x, ..up_left.clone() },
            down_left: Vec3 { x: up_left.x, ..down_right.clone() },
            down_right,
            normal_calc_order: NormalCalcOrder::CounterClockwise,
            side: Side::Bottom,
        }
    }

    fn new_left(creation_spec: &CreationSpec) -> Self
    {
        let up_left = Vec3 {
            x: -(creation_spec.width / 2.0),
            y: creation_spec.height / 2.0,
            z: -(creation_spec.depth / 2.0),
        };

        let down_right = Vec3 {
            x: -(creation_spec.width / 2.0),
            y: -(creation_spec.height / 2.0),
            z: creation_spec.depth / 2.0,
        };

        Self {
            up_left,
            up_right: Vec3 { z: down_right.z, ..up_left.clone() },
            down_left: Vec3 { z: up_left.z, ..down_right.clone() },
            down_right,
            normal_calc_order: NormalCalcOrder::CounterClockwise,
            side: Side::Left,
        }
    }

    fn new_right(creation_spec: &CreationSpec) -> Self
    {
        let up_left = Vec3 {
            x: (creation_spec.width / 2.0),
            y: creation_spec.height / 2.0,
            z: -(creation_spec.depth / 2.0),
        };

        let down_right = Vec3 {
            x: (creation_spec.width / 2.0),
            y: -(creation_spec.height / 2.0),
            z: creation_spec.depth / 2.0,
        };

        Self {
            up_left,
            up_right: Vec3 { z: down_right.z, ..up_left.clone() },
            down_left: Vec3 { z: up_left.z, ..down_right.clone() },
            down_right,
            normal_calc_order: NormalCalcOrder::Clockwise,
            side: Side::Right,
        }
    }

    fn new_back(creation_spec: &CreationSpec) -> Self
    {
        let up_left = Vec3 {
            x: -(creation_spec.width / 2.0),
            y: creation_spec.height / 2.0,
            z: -creation_spec.depth / 2.0,
        };

        let down_right = Vec3 {
            x: creation_spec.width / 2.0,
            y: -(creation_spec.height / 2.0),
            z: -creation_spec.depth / 2.0,
        };

        Self {
            up_left,
            up_right: Vec3 { x: down_right.x, ..up_left.clone() },
            down_left: Vec3 { x: up_left.x, ..down_right.clone() },
            down_right,
            normal_calc_order: NormalCalcOrder::Clockwise,
            side: Side::Back,
        }
    }

    fn new_front(creation_spec: &CreationSpec) -> Self
    {
        let up_left = Vec3 {
            x: -(creation_spec.width / 2.0),
            y: creation_spec.height / 2.0,
            z: creation_spec.depth / 2.0,
        };

        let down_right = Vec3 {
            x: creation_spec.width / 2.0,
            y: -(creation_spec.height / 2.0),
            z: creation_spec.depth / 2.0,
        };

        Self {
            up_left,
            up_right: Vec3 { x: down_right.x, ..up_left.clone() },
            down_left: Vec3 { x: up_left.x, ..down_right.clone() },
            down_right,
            normal_calc_order: NormalCalcOrder::CounterClockwise,
            side: Side::Front,
        }
    }
}

#[derive(Debug)]
enum NormalCalcOrder
{
    Clockwise,
    CounterClockwise,
}

fn create_side(side_positions: &SidePositions, data: &mut Data)
{
    let normal = match side_positions.normal_calc_order {
        NormalCalcOrder::Clockwise => calc_triangle_surface_normal(
            &side_positions.up_left,
            &side_positions.up_right,
            &side_positions.down_left,
        ),
        NormalCalcOrder::CounterClockwise => calc_triangle_surface_normal(
            &side_positions.up_left,
            &side_positions.down_left,
            &side_positions.up_right,
        ),
    };

    data.vertex_data.vertex_normals.push(normal);

    let top_normal_index = data.vertex_data.vertex_normals.len() - 1;

    data.vertex_data
        .vertex_positions
        .push(side_positions.up_right);

    let up_right_pos_index = data.vertex_data.vertex_positions.len() - 1;

    data.vertex_data
        .vertex_positions
        .push(side_positions.up_left);

    let up_left_pos_index = data.vertex_data.vertex_positions.len() - 1;

    data.vertex_data
        .vertex_positions
        .push(side_positions.down_left);

    let down_left_pos_index = data.vertex_data.vertex_positions.len() - 1;

    data.vertex_data
        .vertex_positions
        .push(side_positions.down_right);

    let down_right_pos_index = data.vertex_data.vertex_positions.len() - 1;

    // 🮝
    data.faces.push(Face {
        vertices: [
            FaceVertex {
                pos_index: up_right_pos_index as u32,
                normal_index: top_normal_index as u32,
            },
            FaceVertex {
                pos_index: up_left_pos_index as u32,
                normal_index: top_normal_index as u32,
            },
            FaceVertex {
                pos_index: down_right_pos_index as u32,
                normal_index: top_normal_index as u32,
            },
        ],
        side: side_positions.side,
    });

    // 🮟
    data.faces.push(Face {
        vertices: [
            FaceVertex {
                pos_index: up_left_pos_index as u32,
                normal_index: top_normal_index as u32,
            },
            FaceVertex {
                pos_index: down_left_pos_index as u32,
                normal_index: top_normal_index as u32,
            },
            FaceVertex {
                pos_index: down_right_pos_index as u32,
                normal_index: top_normal_index as u32,
            },
        ],
        side: side_positions.side,
    });
}