//! Bounded Wavefront OBJ ingestion compatible with `ObjMesh.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.

use crate::{Coord, Error, UVCoord, VertexIndexer, ViewerPolygon, ViewerVertex};
use libremetaverse_imaging::DEFAULT_MAX_ENCODED_BYTES;
use libremetaverse_types::compat::StreamReader;
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::Read;

const MAX_OBJ_ELEMENTS: usize = 16_777_216;

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

impl ObjMesh {
    pub fn new_with_stream_reader(reader: StreamReader) -> Result<Self, Error> {
        if reader.0.len() > DEFAULT_MAX_ENCODED_BYTES {
            return Err(Error::Argument);
        }
        let text = String::from_utf8(reader.0).map_err(|_| Error::Parse {
            position: 0,
            context: "OBJ UTF-8",
        })?;
        Self::parse(&text)
    }

    pub fn new_with_string(path: String) -> Result<Self, Error> {
        let metadata = fs::metadata(&path).map_err(|_| Error::InvalidOperation)?;
        if metadata.len() > DEFAULT_MAX_ENCODED_BYTES as u64 {
            return Err(Error::Argument);
        }
        let file = File::open(path).map_err(|_| Error::InvalidOperation)?;
        let limit = u64::try_from(DEFAULT_MAX_ENCODED_BYTES)
            .map_err(|_| Error::Argument)?
            .checked_add(1)
            .ok_or(Error::Argument)?;
        let mut bytes =
            Vec::with_capacity(usize::try_from(metadata.len()).map_err(|_| Error::Argument)?);
        file.take(limit)
            .read_to_end(&mut bytes)
            .map_err(|_| Error::InvalidOperation)?;
        if bytes.len() > DEFAULT_MAX_ENCODED_BYTES {
            return Err(Error::Argument);
        }
        Self::new_with_stream_reader(StreamReader(bytes))
    }

    fn parse(text: &str) -> Result<Self, Error> {
        let mut result = Self::default();
        let mut coords = Vec::new();
        let mut normals = Vec::new();
        let mut uvs = Vec::new();
        let mut face_vertices = Vec::new();
        let mut face_polygons = Vec::new();
        let mut lookup: HashMap<(i32, i32, i32), i32> = HashMap::new();

        for raw in text.lines() {
            let line = raw.trim();
            if line.is_empty() || line.starts_with('#') {
                continue;
            }
            let tokens: Vec<&str> = line.split_whitespace().collect();
            let command = tokens[0];
            if command.eq_ignore_ascii_case("v") {
                if tokens.len() >= 4 {
                    bounded_push(&mut coords, parse_coord(&tokens)?)?;
                }
            } else if command.eq_ignore_ascii_case("vt") {
                if tokens.len() >= 2 {
                    let u = parse_float(tokens[1])?;
                    let v = if tokens.len() > 2 {
                        parse_float(tokens[2])?
                    } else {
                        0.0
                    };
                    bounded_push(&mut uvs, UVCoord { u, v })?;
                }
            } else if command.eq_ignore_ascii_case("vn") {
                if tokens.len() >= 4 {
                    bounded_push(&mut normals, parse_coord(&tokens)?)?;
                }
            } else if command.eq_ignore_ascii_case("o") {
                if tokens.len() > 1 {
                    tokens[1].clone_into(&mut result.mesh_name);
                }
            } else if command.eq_ignore_ascii_case("g") {
                finish_face(
                    &mut result,
                    &mut face_vertices,
                    &mut face_polygons,
                    &mut lookup,
                )?;
            } else if command.eq_ignore_ascii_case("f") && tokens.len() >= 4 {
                let mut polygon = [0_i32; 3];
                for (slot, token) in polygon.iter_mut().zip(&tokens[1..4]) {
                    let mut parts = token.split('/');
                    let position = parse_obj_index(parts.next().unwrap_or_default(), coords.len())?;
                    let texture = parse_optional_obj_index(parts.next(), uvs.len())?;
                    let normal = parse_optional_obj_index(parts.next(), normals.len())?;
                    let key = (
                        to_i32(position)?,
                        optional_i32(texture)?,
                        optional_i32(normal)?,
                    );
                    if let Some(index) = lookup.get(&key) {
                        *slot = *index;
                    } else {
                        let vertex = ViewerVertex {
                            v: coords[position],
                            n: normal.map_or(Coord::default(), |index| normals[index]),
                            uv: texture.map_or(UVCoord::default(), |index| uvs[index]),
                        };
                        bounded_push(&mut face_vertices, vertex)?;
                        let index = to_i32(face_vertices.len() - 1)?;
                        lookup.insert(key, index);
                        *slot = index;
                    }
                }
                bounded_push(
                    &mut face_polygons,
                    ViewerPolygon {
                        v1: polygon[0],
                        v2: polygon[1],
                        v3: polygon[2],
                    },
                )?;
            }
        }
        finish_face(
            &mut result,
            &mut face_vertices,
            &mut face_polygons,
            &mut lookup,
        )?;
        Ok(result)
    }

    pub fn get_vertex_indexer(&self) -> Result<VertexIndexer, Error> {
        let polygons = self.viewer_polygons.iter().cloned().map(Some).collect();
        Ok(VertexIndexer {
            num_prim_faces: self.num_prim_faces,
            viewer_polygons: polygons,
            viewer_vertices: self.viewer_vertices.clone(),
        })
    }
}

fn finish_face(
    mesh: &mut ObjMesh,
    vertices: &mut Vec<ViewerVertex>,
    polygons: &mut Vec<ViewerPolygon>,
    lookup: &mut HashMap<(i32, i32, i32), i32>,
) -> Result<(), Error> {
    if !vertices.is_empty() && !polygons.is_empty() {
        mesh.viewer_vertices.push(std::mem::take(vertices));
        mesh.viewer_polygons.push(std::mem::take(polygons));
        lookup.clear();
        mesh.num_prim_faces = mesh.num_prim_faces.checked_add(1).ok_or(Error::Argument)?;
    }
    Ok(())
}

fn parse_coord(tokens: &[&str]) -> Result<Coord, Error> {
    Ok(Coord {
        x: parse_float(tokens[1])?,
        y: parse_float(tokens[2])?,
        z: parse_float(tokens[3])?,
    })
}

fn parse_float(value: &str) -> Result<f32, Error> {
    let value = value.parse::<f32>().map_err(|_| Error::Parse {
        position: 0,
        context: "OBJ number",
    })?;
    if value.is_finite() {
        Ok(value)
    } else {
        Err(Error::Argument)
    }
}

fn parse_obj_index(value: &str, len: usize) -> Result<usize, Error> {
    let one_based = value.parse::<i32>().map_err(|_| Error::Parse {
        position: 0,
        context: "OBJ index",
    })?;
    let index = one_based.checked_sub(1).ok_or(Error::IndexOutOfRange)?;
    let index = usize::try_from(index).map_err(|_| Error::IndexOutOfRange)?;
    if index < len {
        Ok(index)
    } else {
        Err(Error::IndexOutOfRange)
    }
}

fn parse_optional_obj_index(value: Option<&str>, len: usize) -> Result<Option<usize>, Error> {
    match value {
        None | Some("") => Ok(None),
        Some(value) => parse_obj_index(value, len).map(Some),
    }
}

fn bounded_push<T>(values: &mut Vec<T>, value: T) -> Result<(), Error> {
    if values.len() >= MAX_OBJ_ELEMENTS {
        return Err(Error::Argument);
    }
    values.push(value);
    Ok(())
}

fn to_i32(value: usize) -> Result<i32, Error> {
    i32::try_from(value).map_err(|_| Error::Argument)
}

fn optional_i32(value: Option<usize>) -> Result<i32, Error> {
    value.map_or(Ok(-1), to_i32)
}

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

    const GOLDEN: &str = "\
o sculpt_part\n\
v 0 0 0\n\
v 1 0 0\n\
v 0 1 0\n\
v 0 0 1\n\
vt 0 0\n\
vt 1 0\n\
vt 0 1\n\
vn 0 0 1\n\
g front\n\
f 1/1/1 2/2/1 3/3/1\n\
f 1/1/1 3/3/1 2/2/1\n\
g side\n\
f 1//1 2//1 4//1\n";

    #[test]
    fn obj_golden_groups_vertices_uvs_and_normals_deterministically() {
        let first = ObjMesh::new_with_stream_reader(StreamReader(GOLDEN.as_bytes().to_vec()))
            .expect("OBJ parse");
        let second = ObjMesh::new_with_stream_reader(StreamReader(GOLDEN.as_bytes().to_vec()))
            .expect("OBJ parse");
        assert_eq!(first, second);
        assert_eq!(first.mesh_name, "sculpt_part");
        assert_eq!(first.num_prim_faces, 2);
        assert_eq!(first.viewer_polygons[0].len(), 2);
        assert_eq!(first.viewer_vertices[0].len(), 3);
        assert_eq!(
            first.viewer_polygons[1],
            vec![ViewerPolygon {
                v1: 0,
                v2: 1,
                v3: 2
            }]
        );
        assert_eq!(first.viewer_vertices[1][0].uv, UVCoord::default());
        let indexer = first.get_vertex_indexer().expect("indexer");
        assert_eq!(indexer.num_prim_faces, 2);
        assert_eq!(indexer.viewer_polygons[0].as_ref().expect("face").len(), 2);
    }

    #[test]
    fn obj_ignores_non_triangle_tail_like_reference() {
        let source = "v 0 0 0\nv 1 0 0\nv 0 1 0\nv 0 0 1\nf 1 2 3 4\n";
        let mesh = ObjMesh::new_with_stream_reader(StreamReader(source.as_bytes().to_vec()))
            .expect("OBJ parse");
        assert_eq!(
            mesh.viewer_polygons[0],
            vec![ViewerPolygon {
                v1: 0,
                v2: 1,
                v3: 2
            }]
        );
    }

    #[test]
    fn malformed_obj_returns_typed_errors_without_panicking() {
        let malformed = "v 0 0 0\nv 1 0 0\nf 1 2 9\n";
        assert_eq!(
            ObjMesh::new_with_stream_reader(StreamReader(malformed.as_bytes().to_vec())),
            Err(Error::IndexOutOfRange)
        );
        let malformed = "v NaN 0 0\n";
        assert_eq!(
            ObjMesh::new_with_stream_reader(StreamReader(malformed.as_bytes().to_vec())),
            Err(Error::Argument)
        );
        assert!(matches!(
            ObjMesh::new_with_stream_reader(StreamReader(vec![0xff])),
            Err(Error::Parse {
                context: "OBJ UTF-8",
                ..
            })
        ));
    }
}
