//! Deterministic reference renderer built on the checked native prim mesher.

#![allow(clippy::cast_possible_truncation)] // Checked C# float-to-int twist conversion.
#![allow(clippy::missing_errors_doc)] // Public signatures are fixed by the API mapping.
#![allow(clippy::must_use_candidate)] // Attributes are not part of the mapped surface.
#![allow(clippy::needless_pass_by_value)] // Mapped value parameters are owned.
#![allow(clippy::unnecessary_wraps)] // The mapped constructor is fallible in the C# API.
#![allow(clippy::unused_self)] // IRendering exposes these operations as instance methods.

use libremetaverse::rendering::{
    DetailLevel, Face, FaceMask, FacetedMesh, IRendering, SimpleMesh, Vertex,
};
use libremetaverse::{MappingType, Primitive, PrimitiveTextureEntryFace};
use libremetaverse_imaging::ManagedImage;
use libremetaverse_prim_mesher::{
    PathType, PrimMesh, SculptMesh, SculptMeshSculptType, VertexIndexer,
};
use libremetaverse_types::compat::Object;
use libremetaverse_types::{
    Error, HoleType, PathCurve, ProfileCurve, SculptType, Vector2, Vector3,
};

const MAX_FACE_VERTICES: usize = 65_536;
const MAX_TOTAL_VERTICES: usize = MAX_FACE_VERTICES;
const MAX_TOTAL_INDICES: usize = MAX_TOTAL_VERTICES * 6;

/// Small, deterministic renderer for prim and sculpt geometry.
pub struct SimpleRenderer;

impl SimpleRenderer {
    pub(crate) fn native_new() -> Result<Self, Error> {
        Ok(Self)
    }

    pub(crate) fn native_generate_simple_mesh(
        &self,
        prim: Primitive,
        lod: DetailLevel,
    ) -> Result<SimpleMesh, Error> {
        let source = prim.id;
        let faceted = self.native_generate_faceted_mesh(prim, lod)?;
        flatten_faces(faceted.faces, source)
    }

    pub(crate) fn native_generate_simple_sculpt_mesh(
        &self,
        prim: Primitive,
        sculpt_texture: ManagedImage,
        lod: DetailLevel,
    ) -> Result<SimpleMesh, Error> {
        let source = prim.id;
        let faceted = self.native_generate_faceted_sculpt_mesh(prim, sculpt_texture, lod)?;
        flatten_faces(faceted.faces, source)
    }

    pub(crate) fn native_generate_faceted_mesh(
        &self,
        prim: Primitive,
        lod: DetailLevel,
    ) -> Result<FacetedMesh, Error> {
        let source = prim.id;
        let mesh = generate_prim_mesh(&prim, lod, source)?;
        let indexer = mesh
            .get_vertex_indexer()
            .map_err(|_| render_error(source, "index primitive viewer faces"))?
            .ok_or_else(|| render_error(source, "primitive viewer faces are unavailable"))?;
        let faces = indexed_faces(&prim, indexer, source)?;
        Ok(FacetedMesh {
            faces,
            skin_data: None,
        })
    }

    pub(crate) fn native_generate_faceted_sculpt_mesh(
        &self,
        prim: Primitive,
        sculpt_texture: ManagedImage,
        lod: DetailLevel,
    ) -> Result<FacetedMesh, Error> {
        let source = prim.id;
        let sculpt = prim
            .sculpt
            .as_ref()
            .ok_or_else(|| render_error(source, "primitive has no sculpt metadata"))?;
        let sculpt_type = match sculpt.type_() {
            SculptType::Cylinder => SculptMeshSculptType::Cylinder,
            SculptType::Plane => SculptMeshSculptType::Plane,
            SculptType::Sphere => SculptMeshSculptType::Sphere,
            SculptType::Torus => SculptMeshSculptType::Torus,
            SculptType::Mesh => {
                return Err(render_error(
                    source,
                    "mesh sculpt requires a decoded mesh asset",
                ));
            }
            SculptType::None | SculptType::Invert | SculptType::Mirror => {
                return Err(render_error(source, "unsupported sculpt topology"));
            }
        };
        let mesher_lod = match lod {
            DetailLevel::Highest | DetailLevel::High => 32,
            DetailLevel::Medium => 16,
            DetailLevel::Low => 8,
        };
        let mesh = SculptMesh::new_with_managed_image_sculpt_type_int32_boolean_boolean_boolean(
            sculpt_texture,
            sculpt_type,
            mesher_lod,
            true,
            sculpt.mirror(),
            sculpt.invert(),
        )
        .map_err(|_| render_error(source, "generate bounded sculpt geometry"))?;
        if mesh.coords.len() > MAX_FACE_VERTICES
            || mesh.coords.len() != mesh.normals.len()
            || mesh.coords.len() != mesh.uvs.len()
        {
            return Err(render_error(source, "invalid sculpt vertex domains"));
        }
        let vertices = mesh
            .coords
            .iter()
            .zip(&mesh.normals)
            .zip(&mesh.uvs)
            .map(|((position, normal), uv)| {
                checked_vertex(
                    Vector3 {
                        x: position.x,
                        y: position.y,
                        z: position.z,
                    },
                    Vector3 {
                        x: normal.x,
                        y: normal.y,
                        z: normal.z,
                    },
                    Vector2 { x: uv.u, y: uv.v },
                    source,
                )
            })
            .collect::<Result<Vec<_>, _>>()?;
        let mut indices = Vec::with_capacity(
            mesh.faces
                .len()
                .checked_mul(3)
                .ok_or_else(|| render_error(source, "sculpt index count overflow"))?,
        );
        for face in mesh.faces {
            push_triangle(
                &mut indices,
                face.v1,
                face.v2,
                face.v3,
                vertices.len(),
                source,
            )?;
        }
        let texture = texture_for_face(&prim, 0, source)?;
        let face = make_face(0, vertices, indices, texture, source)?;
        Ok(FacetedMesh {
            faces: vec![face],
            skin_data: None,
        })
    }

    pub(crate) fn native_transform_tex_coords(
        &self,
        vertices: &mut Vec<Vertex>,
        center: Vector3,
        te_face: PrimitiveTextureEntryFace,
        prim_scale: Vector3,
    ) -> Result<(), Error> {
        if !finite3(center)
            || !finite3(prim_scale)
            || ![
                te_face.repeat_u(),
                te_face.repeat_v(),
                te_face.offset_u(),
                te_face.offset_v(),
                te_face.rotation(),
            ]
            .into_iter()
            .all(f32::is_finite)
        {
            return Err(Error::Argument);
        }
        let cosine = te_face.rotation().cos();
        let sine = te_face.rotation().sin();
        for vertex in vertices {
            if !finite3(vertex.position) || !finite3(vertex.normal) || !finite2(vertex.tex_coord) {
                return Err(Error::Argument);
            }
            if te_face.tex_map_type() == MappingType::Planar {
                let mut binormal;
                let normal_x = vertex.normal.x;
                if (-0.5..0.5).contains(&normal_x) {
                    binormal = Vector3::unit_x();
                    if vertex.normal.y > 0.0 {
                        binormal.x = -1.0;
                    }
                } else {
                    binormal = Vector3::unit_y();
                    if normal_x < 0.0 {
                        binormal.y = -1.0;
                    }
                }
                let tangent = cross(binormal, vertex.normal);
                let scaled = Vector3 {
                    x: vertex.position.x * prim_scale.x,
                    y: vertex.position.y * prim_scale.y,
                    z: vertex.position.z * prim_scale.z,
                };
                vertex.tex_coord.x = 0.5 + dot(binormal, scaled) * 2.0;
                vertex.tex_coord.y = 0.5 - dot(tangent, scaled) * 2.0;
            }
            let x = vertex.tex_coord.x - 0.5;
            let y = vertex.tex_coord.y - 0.5;
            vertex.tex_coord.x =
                (x * cosine + y * sine) * te_face.repeat_u() + te_face.offset_u() + 0.5;
            vertex.tex_coord.y =
                (-x * sine + y * cosine) * te_face.repeat_v() + te_face.offset_v() + 0.5;
            if !finite2(vertex.tex_coord) {
                return Err(Error::Argument);
            }
        }
        Ok(())
    }
}

impl IRendering for SimpleRenderer {
    fn generate_faceted_mesh(
        &self,
        prim: Primitive,
        lod: DetailLevel,
    ) -> Result<Option<FacetedMesh>, Error> {
        self.native_generate_faceted_mesh(prim, lod).map(Some)
    }

    fn generate_faceted_sculpt_mesh(
        &self,
        prim: Primitive,
        sculpt_texture: ManagedImage,
        lod: DetailLevel,
    ) -> Result<Option<FacetedMesh>, Error> {
        self.native_generate_faceted_sculpt_mesh(prim, sculpt_texture, lod)
            .map(Some)
    }

    fn generate_simple_mesh(
        &self,
        prim: Primitive,
        lod: DetailLevel,
    ) -> Result<Option<SimpleMesh>, Error> {
        self.native_generate_simple_mesh(prim, lod).map(Some)
    }

    fn generate_simple_sculpt_mesh(
        &self,
        prim: Primitive,
        sculpt_texture: ManagedImage,
        lod: DetailLevel,
    ) -> Result<Option<SimpleMesh>, Error> {
        self.native_generate_simple_sculpt_mesh(prim, sculpt_texture, lod)
            .map(Some)
    }

    fn transform_tex_coords(
        &self,
        vertices: &mut Vec<Vertex>,
        center: Vector3,
        te_face: PrimitiveTextureEntryFace,
        prim_scale: Vector3,
    ) -> Result<(), Error> {
        self.native_transform_tex_coords(vertices, center, te_face, prim_scale)
    }
}

fn generate_prim_mesh(
    prim: &Primitive,
    lod: DetailLevel,
    source: libremetaverse_types::UUID,
) -> Result<PrimMesh, Error> {
    let data = &prim.prim_data;
    let parameters = [
        data.profile_begin,
        data.profile_end,
        data.profile_hollow,
        data.path_scale_x,
        data.path_scale_y,
        data.path_begin,
        data.path_end,
        data.path_shear_x,
        data.path_shear_y,
        data.path_radius_offset,
        data.path_revolutions,
        data.path_skew,
        data.path_taper_x,
        data.path_taper_y,
        data.path_twist_begin,
        data.path_twist,
    ];
    if !parameters.into_iter().all(f32::is_finite) {
        return Err(render_error(
            source,
            "non-finite primitive construction data",
        ));
    }
    let (mut sides, mut profile_begin, mut profile_end, sphere_mode) =
        match data.profile_curve_with_property() {
            ProfileCurve::Circle => (lod_sides(lod), data.profile_begin, data.profile_end, false),
            ProfileCurve::EqualTriangle
            | ProfileCurve::IsoTriangle
            | ProfileCurve::RightTriangle => (3, data.profile_begin, data.profile_end, false),
            ProfileCurve::HalfCircle => (
                lod_sides(lod),
                data.profile_begin.mul_add(0.5, 0.5),
                data.profile_end.mul_add(0.5, 0.5),
                true,
            ),
            ProfileCurve::Square => (4, data.profile_begin, data.profile_end, false),
        };
    if sides < 3 {
        sides = 3;
    }
    profile_begin = profile_begin.clamp(0.0, 1.0);
    profile_end = profile_end.clamp(0.0, 1.0);
    let hollow_sides = if data.profile_hole() == HoleType::Circle {
        lod_sides(lod)
    } else if data.profile_hole() == HoleType::Triangle {
        3
    } else if data.profile_hole() == HoleType::Same {
        sides
    } else {
        4
    };
    let mut mesh = PrimMesh::new(
        sides,
        profile_begin,
        profile_end,
        data.profile_hollow,
        hollow_sides,
    )
    .map_err(|_| render_error(source, "initialize primitive profile"))?;
    mesh.viewer_mode = true;
    mesh.sphere_mode = sphere_mode;
    mesh.hole_size_x = data.path_scale_x;
    mesh.hole_size_y = data.path_scale_y;
    mesh.path_cut_begin = data.path_begin;
    mesh.path_cut_end = data.path_end;
    mesh.top_shear_x = data.path_shear_x;
    mesh.top_shear_y = data.path_shear_y;
    mesh.radius = data.path_radius_offset;
    mesh.revolutions = data.path_revolutions;
    mesh.skew = data.path_skew;
    mesh.steps_per_revolution = lod_sides(lod);
    let path_type = if matches!(data.path_curve, PathCurve::Line | PathCurve::Flexible) {
        mesh.taper_x = 1.0 - data.path_scale_x;
        mesh.taper_y = 1.0 - data.path_scale_y;
        mesh.twist_begin = checked_twist(data.path_twist_begin, 180.0, source)?;
        mesh.twist_end = checked_twist(data.path_twist, 180.0, source)?;
        PathType::Linear
    } else {
        mesh.taper_x = data.path_taper_x;
        mesh.taper_y = data.path_taper_y;
        mesh.twist_begin = checked_twist(data.path_twist_begin, 360.0, source)?;
        mesh.twist_end = checked_twist(data.path_twist, 360.0, source)?;
        PathType::Circular
    };
    mesh.extrude(path_type)
        .map_err(|_| render_error(source, "extrude primitive geometry"))?;
    if mesh.coords.len() > MAX_TOTAL_VERTICES || mesh.viewer_faces.len() > MAX_TOTAL_INDICES / 3 {
        return Err(render_error(
            source,
            "primitive geometry exceeds renderer bounds",
        ));
    }
    Ok(mesh)
}

fn indexed_faces(
    prim: &Primitive,
    indexer: VertexIndexer,
    source: libremetaverse_types::UUID,
) -> Result<Vec<Face>, Error> {
    let count = usize::try_from(indexer.num_prim_faces)
        .map_err(|_| render_error(source, "invalid primitive face count"))?;
    if count != indexer.viewer_vertices.len() || count != indexer.viewer_polygons.len() {
        return Err(render_error(source, "misaligned primitive face domains"));
    }
    let mut total_vertices = 0usize;
    let mut total_indices = 0usize;
    let mut faces = Vec::with_capacity(count);
    for face_index in 0..count {
        let source_vertices = &indexer.viewer_vertices[face_index];
        let polygons = indexer.viewer_polygons[face_index]
            .as_ref()
            .ok_or_else(|| render_error(source, "primitive face has no polygon domain"))?;
        if source_vertices.is_empty() {
            if polygons.is_empty() {
                continue;
            }
            return Err(render_error(
                source,
                "primitive face polygons have no vertex domain",
            ));
        }
        if source_vertices.len() > MAX_FACE_VERTICES {
            return Err(render_error(
                source,
                "primitive face exceeds 16-bit vertex range",
            ));
        }
        total_vertices = total_vertices
            .checked_add(source_vertices.len())
            .ok_or_else(|| render_error(source, "primitive vertex count overflow"))?;
        if total_vertices > MAX_TOTAL_VERTICES {
            return Err(render_error(source, "primitive vertex budget exceeded"));
        }
        let vertices = source_vertices
            .iter()
            .map(|vertex| {
                checked_vertex(
                    Vector3 {
                        x: vertex.v.x,
                        y: vertex.v.y,
                        z: vertex.v.z,
                    },
                    Vector3 {
                        x: vertex.n.x,
                        y: vertex.n.y,
                        z: vertex.n.z,
                    },
                    Vector2 {
                        x: vertex.uv.u,
                        y: 1.0 - vertex.uv.v,
                    },
                    source,
                )
            })
            .collect::<Result<Vec<_>, _>>()?;
        let mut indices = Vec::with_capacity(
            polygons
                .len()
                .checked_mul(3)
                .ok_or_else(|| render_error(source, "primitive index count overflow"))?,
        );
        for polygon in polygons {
            if polygon.v1 == polygon.v2 || polygon.v1 == polygon.v3 || polygon.v2 == polygon.v3 {
                continue;
            }
            push_triangle(
                &mut indices,
                polygon.v1,
                polygon.v2,
                polygon.v3,
                vertices.len(),
                source,
            )?;
        }
        total_indices = total_indices
            .checked_add(indices.len())
            .ok_or_else(|| render_error(source, "primitive index count overflow"))?;
        if total_indices > MAX_TOTAL_INDICES {
            return Err(render_error(source, "primitive index budget exceeded"));
        }
        let texture = texture_for_face(prim, face_index, source)?;
        faces.push(make_face(
            i32::try_from(face_index)
                .map_err(|_| render_error(source, "primitive face id overflow"))?,
            vertices,
            indices,
            texture,
            source,
        )?);
    }
    Ok(faces)
}

fn flatten_faces(
    faces: Vec<Face>,
    source: libremetaverse_types::UUID,
) -> Result<SimpleMesh, Error> {
    let mut vertices = Vec::new();
    let mut indices = Vec::new();
    for face in faces {
        let base = vertices.len();
        let combined = base
            .checked_add(face.vertices.len())
            .ok_or_else(|| render_error(source, "simple mesh vertex count overflow"))?;
        if combined > MAX_TOTAL_VERTICES {
            return Err(render_error(
                source,
                "simple mesh exceeds 16-bit vertex range",
            ));
        }
        for index in face.indices {
            let global = base
                .checked_add(usize::from(index))
                .ok_or_else(|| render_error(source, "simple mesh index overflow"))?;
            if global >= combined {
                return Err(render_error(source, "simple mesh index is out of range"));
            }
            indices.push(
                u16::try_from(global)
                    .map_err(|_| render_error(source, "simple mesh index exceeds 16 bits"))?,
            );
        }
        vertices.extend(face.vertices);
    }
    if indices.len() > MAX_TOTAL_INDICES {
        return Err(render_error(source, "simple mesh index budget exceeded"));
    }
    Ok(SimpleMesh { indices, vertices })
}

fn texture_for_face(
    prim: &Primitive,
    index: usize,
    source: libremetaverse_types::UUID,
) -> Result<PrimitiveTextureEntryFace, Error> {
    let Some(textures) = prim.textures.as_ref() else {
        return Ok(PrimitiveTextureEntryFace::default());
    };
    let index =
        u32::try_from(index).map_err(|_| render_error(source, "texture face index overflow"))?;
    textures
        .get_face(index)
        .map_err(|_| render_error(source, "texture face index is out of range"))
        .map(|face| face.cloned().unwrap_or_default())
}

fn make_face(
    id: i32,
    vertices: Vec<Vertex>,
    indices: Vec<u16>,
    texture_face: PrimitiveTextureEntryFace,
    source: libremetaverse_types::UUID,
) -> Result<Face, Error> {
    let first = vertices
        .first()
        .ok_or_else(|| render_error(source, "render face has no vertices"))?;
    let mut min = first.position;
    let mut max = first.position;
    for vertex in &vertices[1..] {
        min.x = min.x.min(vertex.position.x);
        min.y = min.y.min(vertex.position.y);
        min.z = min.z.min(vertex.position.z);
        max.x = max.x.max(vertex.position.x);
        max.y = max.y.max(vertex.position.y);
        max.z = max.z.max(vertex.position.z);
    }
    let center = Vector3 {
        x: (min.x + max.x) * 0.5,
        y: (min.y + max.y) * 0.5,
        z: (min.z + max.z) * 0.5,
    };
    Ok(Face {
        begin_s: 0,
        begin_t: 0,
        center,
        edge: Vec::new(),
        id,
        indices,
        mask: FaceMask::SINGLE,
        max_extent: max,
        min_extent: min,
        normalized_scale: Vector3 {
            x: max.x - min.x,
            y: max.y - min.y,
            z: max.z - min.z,
        },
        num_s: 0,
        num_t: 0,
        tex_coords1: None,
        texture_face,
        user_data: Object::Undefined,
        vertices,
        weights: None,
    })
}

fn checked_vertex(
    position: Vector3,
    normal: Vector3,
    tex_coord: Vector2,
    source: libremetaverse_types::UUID,
) -> Result<Vertex, Error> {
    if !finite3(position) || !finite3(normal) || !finite2(tex_coord) {
        return Err(render_error(
            source,
            "renderer produced a non-finite vertex",
        ));
    }
    Ok(Vertex {
        position,
        normal,
        tex_coord,
    })
}

fn push_triangle(
    indices: &mut Vec<u16>,
    v1: i32,
    v2: i32,
    v3: i32,
    vertex_count: usize,
    source: libremetaverse_types::UUID,
) -> Result<(), Error> {
    for index in [v1, v2, v3] {
        let index =
            usize::try_from(index).map_err(|_| render_error(source, "negative renderer index"))?;
        if index >= vertex_count {
            return Err(render_error(source, "renderer index is out of range"));
        }
        indices.push(
            u16::try_from(index)
                .map_err(|_| render_error(source, "renderer index exceeds 16 bits"))?,
        );
    }
    Ok(())
}

fn checked_twist(
    value: f32,
    multiplier: f32,
    source: libremetaverse_types::UUID,
) -> Result<i32, Error> {
    let degrees = value * multiplier;
    if !degrees.is_finite() || degrees.abs() > 360_000.0 {
        return Err(render_error(source, "primitive twist is out of range"));
    }
    Ok(degrees as i32)
}

const fn lod_sides(lod: DetailLevel) -> i32 {
    match lod {
        DetailLevel::Low => 6,
        DetailLevel::Medium => 12,
        DetailLevel::High | DetailLevel::Highest => 24,
    }
}

const fn render_error(source: libremetaverse_types::UUID, context: &'static str) -> Error {
    Error::Rendering { source, context }
}

const fn finite2(value: Vector2) -> bool {
    value.x.is_finite() && value.y.is_finite()
}

const fn finite3(value: Vector3) -> bool {
    value.x.is_finite() && value.y.is_finite() && value.z.is_finite()
}

const fn dot(left: Vector3, right: Vector3) -> f32 {
    left.x * right.x + left.y * right.y + left.z * right.z
}

const fn cross(left: Vector3, right: Vector3) -> Vector3 {
    Vector3 {
        x: left.y * right.z - left.z * right.y,
        y: left.z * right.x - left.x * right.z,
        z: left.x * right.y - left.y * right.x,
    }
}
