//! Viewer-face indexing compatible with `VertexIndexer.cs`.

#![allow(clippy::missing_errors_doc)] // Result shapes are fixed by the compatibility map.
#![allow(clippy::must_use_candidate)] // Attributes are not part of the mapped C# surface.
#![allow(clippy::needless_pass_by_value)] // PrimMesh ownership matches the mapped constructor.

use crate::{Coord, Error, PrimMesh, UVCoord};
use std::collections::HashMap;

const MAX_PRIM_FACES: usize = 65_536;

#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct ViewerVertex {
    pub n: Coord,
    pub uv: UVCoord,
    pub v: Coord,
}

impl ViewerVertex {
    pub fn new(coord: Coord, normal: Coord, uv: UVCoord) -> Result<Self, Error> {
        Ok(Self {
            n: normal,
            uv,
            v: coord,
        })
    }
}

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct ViewerPolygon {
    pub v1: i32,
    pub v2: i32,
    pub v3: i32,
}

impl ViewerPolygon {
    pub fn new(v1: i32, v2: i32, v3: i32) -> Result<Self, Error> {
        Ok(Self { v1, v2, v3 })
    }
}

#[derive(Clone, Debug, Default, PartialEq)]
pub struct VertexIndexer {
    pub num_prim_faces: i32,
    pub viewer_polygons: Vec<Option<Vec<ViewerPolygon>>>,
    pub viewer_vertices: Vec<Vec<ViewerVertex>>,
}

impl VertexIndexer {
    pub fn new_with_constructor() -> Result<Self, Error> {
        Ok(Self::default())
    }

    pub fn new_with_prim_mesh(prim_mesh: PrimMesh) -> Result<Self, Error> {
        Self::from_viewer_faces(&prim_mesh.viewer_faces)
    }

    pub(crate) fn from_viewer_faces(faces: &[crate::ViewerFace]) -> Result<Self, Error> {
        let max_face = faces
            .iter()
            .map(|face| face.prim_face_number)
            .max()
            .unwrap_or(0);
        let num_prim_faces = max_face.checked_add(1).ok_or(Error::Argument)?;
        let face_count = usize::try_from(num_prim_faces).map_err(|_| Error::Argument)?;
        if face_count > MAX_PRIM_FACES {
            return Err(Error::Argument);
        }
        let mut viewer_vertices: Vec<Vec<ViewerVertex>> =
            (0..face_count).map(|_| Vec::new()).collect();
        let mut viewer_polygons: Vec<Option<Vec<ViewerPolygon>>> =
            (0..face_count).map(|_| Some(Vec::new())).collect();
        let mut indices: Vec<HashMap<i32, i32>> = (0..face_count).map(|_| HashMap::new()).collect();

        for face in faces {
            let face_index =
                usize::try_from(face.prim_face_number).map_err(|_| Error::IndexOutOfRange)?;
            if face_index >= face_count {
                return Err(Error::IndexOutOfRange);
            }
            let vertices = &mut viewer_vertices[face_index];
            let map = &mut indices[face_index];
            let v1 = indexed_vertex(map, vertices, face.coord_index1, face.v1, face.n1, face.uv1)?;
            let v2 = indexed_vertex(map, vertices, face.coord_index2, face.v2, face.n2, face.uv2)?;
            let v3 = indexed_vertex(map, vertices, face.coord_index3, face.v3, face.n3, face.uv3)?;
            viewer_polygons[face_index]
                .as_mut()
                .ok_or(Error::InvalidOperation)?
                .push(ViewerPolygon { v1, v2, v3 });
        }
        Ok(Self {
            num_prim_faces,
            viewer_polygons,
            viewer_vertices,
        })
    }
}

fn indexed_vertex(
    indices: &mut HashMap<i32, i32>,
    vertices: &mut Vec<ViewerVertex>,
    coord_index: i32,
    v: Coord,
    n: Coord,
    uv: UVCoord,
) -> Result<i32, Error> {
    if coord_index < 0 {
        return Err(Error::IndexOutOfRange);
    }
    if let Some(index) = indices.get(&coord_index) {
        return Ok(*index);
    }
    let index = i32::try_from(vertices.len()).map_err(|_| Error::Argument)?;
    vertices.push(ViewerVertex { n, uv, v });
    indices.insert(coord_index, index);
    Ok(index)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ViewerFace;

    #[test]
    fn direct_constructor_deduplicates_coordinate_indices_per_prim_face() {
        let mut first = ViewerFace::new(0).expect("face");
        first.coord_index1 = 0;
        first.coord_index2 = 1;
        first.coord_index3 = 2;
        first.v1 = Coord {
            x: 0.0,
            y: 0.0,
            z: 0.0,
        };
        first.v2 = Coord {
            x: 1.0,
            y: 0.0,
            z: 0.0,
        };
        first.v3 = Coord {
            x: 0.0,
            y: 1.0,
            z: 0.0,
        };
        let mut second = first;
        second.coord_index2 = 2;
        second.coord_index3 = 3;
        let indexer = VertexIndexer::from_viewer_faces(&[first, second]).expect("indexer");
        assert_eq!(indexer.num_prim_faces, 1);
        assert_eq!(indexer.viewer_vertices[0].len(), 4);
        assert_eq!(
            indexer.viewer_polygons[0].as_ref().expect("polygons"),
            &vec![
                ViewerPolygon {
                    v1: 0,
                    v2: 1,
                    v3: 2
                },
                ViewerPolygon {
                    v1: 0,
                    v2: 2,
                    v3: 3
                },
            ]
        );
    }
}
