#![allow(
    clippy::cast_possible_truncation,
    clippy::cast_sign_loss,
    clippy::float_cmp,
    clippy::too_many_lines
)] // Fixture quantization is explicitly clamped to the u16 domain.

use std::collections::{BTreeMap, HashMap};
use std::io::Write as _;

use flate2::{Compression, write::ZlibEncoder};
use libremetaverse::import_export::{ModelFace, ModelPrim};
use libremetaverse::rendering::{DetailLevel, RiggedSkinMath, SimpleMesh, Vertex};
use libremetaverse::{Primitive, PrimitiveSculptData, PrimitiveTextureEntry};
use libremetaverse_rendering_mesh_foundry::{MeshFaceAux, MeshFoundry};
use libremetaverse_structured_data::{OSD, OSDParser};
use libremetaverse_types::{Error, SculptType, UUID, Vector2, Vector3};

fn compress(value: OSD) -> Vec<u8> {
    let bytes = OSDParser::serialize_llsd_binary_with_osd_boolean(value, false)
        .expect("serialize fixture section");
    let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
    encoder.write_all(&bytes).expect("compress fixture section");
    encoder.finish().expect("finish fixture section")
}

fn pack(sections: BTreeMap<&str, OSD>) -> Vec<u8> {
    let mut payload = Vec::new();
    let mut header = HashMap::new();
    for (name, value) in sections {
        let section = compress(value);
        header.insert(
            name.to_owned(),
            OSD::Map(HashMap::from([
                (
                    "offset".to_owned(),
                    OSD::Integer(i32::try_from(payload.len()).expect("fixture offset")),
                ),
                (
                    "size".to_owned(),
                    OSD::Integer(i32::try_from(section.len()).expect("fixture size")),
                ),
            ])),
        );
        payload.extend(section);
    }
    header.insert("version".to_owned(), OSD::Integer(1));
    let mut asset = OSDParser::serialize_llsd_binary_with_osd_boolean(OSD::Map(header), false)
        .expect("serialize fixture header");
    asset.extend(payload);
    asset
}

fn quantized3(values: &[[f32; 3]], min: [f32; 3], max: [f32; 3]) -> Vec<u8> {
    values
        .iter()
        .flat_map(|value| {
            (0..3).flat_map(|axis| quantize(value[axis], min[axis], max[axis]).to_le_bytes())
        })
        .collect()
}

fn quantized2(values: &[[f32; 2]], min: [f32; 2], max: [f32; 2]) -> Vec<u8> {
    values
        .iter()
        .flat_map(|value| {
            (0..2).flat_map(|axis| quantize(value[axis], min[axis], max[axis]).to_le_bytes())
        })
        .collect()
}

fn quantize(value: f32, min: f32, max: f32) -> u16 {
    (((value - min) / (max - min)).clamp(0.0, 1.0) * 65_535.0).round() as u16
}

fn domain2(min: Vector2, max: Vector2) -> OSD {
    OSD::Map(HashMap::from([
        (
            "Min".to_owned(),
            OSD::from_vector2(min).expect("domain min"),
        ),
        (
            "Max".to_owned(),
            OSD::from_vector2(max).expect("domain max"),
        ),
    ]))
}

fn domain3(min: Vector3, max: Vector3) -> OSD {
    OSD::Map(HashMap::from([
        (
            "Min".to_owned(),
            OSD::from_vector3(min).expect("domain min"),
        ),
        (
            "Max".to_owned(),
            OSD::from_vector3(max).expect("domain max"),
        ),
    ]))
}

fn identity() -> OSD {
    OSD::Array(
        [
            1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
        ]
        .into_iter()
        .map(OSD::Real)
        .collect(),
    )
}

fn rigged_submesh(triangle: [u16; 3], weights: Vec<u8>) -> OSD {
    let positions = [[-0.5, -0.5, 0.0], [0.5, -0.5, 0.0], [-0.5, 0.5, 0.0]];
    let normals = [[0.0, 0.0, 1.0]; 3];
    let uv0 = [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]];
    let uv1 = [[0.25, 0.25], [0.75, 0.25], [0.25, 0.75]];
    let tangents = [[1.0, 0.0, 0.0, -1.0]; 3];
    let tangent_bytes: Vec<u8> = tangents
        .iter()
        .flat_map(|value| {
            value
                .iter()
                .flat_map(|component| quantize(*component, -1.0, 1.0).to_le_bytes())
        })
        .collect();
    let triangle_bytes: Vec<u8> = triangle.into_iter().flat_map(u16::to_le_bytes).collect();
    let mut map = HashMap::from([
        (
            "PositionDomain".to_owned(),
            domain3(
                Vector3 {
                    x: -0.5,
                    y: -0.5,
                    z: -0.5,
                },
                Vector3 {
                    x: 0.5,
                    y: 0.5,
                    z: 0.5,
                },
            ),
        ),
        (
            "Position".to_owned(),
            OSD::Binary(quantized3(&positions, [-0.5; 3], [0.5; 3])),
        ),
        (
            "Normal".to_owned(),
            OSD::Binary(quantized3(&normals, [-1.0; 3], [1.0; 3])),
        ),
        (
            "TexCoord0Domain".to_owned(),
            domain2(Vector2::zero(), Vector2 { x: 1.0, y: 1.0 }),
        ),
        (
            "TexCoord0".to_owned(),
            OSD::Binary(quantized2(&uv0, [0.0; 2], [1.0; 2])),
        ),
        (
            "TexCoord1Domain".to_owned(),
            domain2(Vector2::zero(), Vector2 { x: 1.0, y: 1.0 }),
        ),
        (
            "TexCoord1".to_owned(),
            OSD::Binary(quantized2(&uv1, [0.0; 2], [1.0; 2])),
        ),
        ("Tangent".to_owned(), OSD::Binary(tangent_bytes)),
        ("TriangleList".to_owned(), OSD::Binary(triangle_bytes)),
        (
            "NormalizedScale".to_owned(),
            OSD::from_vector3(Vector3 {
                x: 2.0,
                y: 3.0,
                z: 4.0,
            })
            .expect("normalized scale"),
        ),
    ]);
    if !weights.is_empty() {
        map.insert("Weights".to_owned(), OSD::Binary(weights));
    }
    OSD::Map(map)
}

fn skin() -> OSD {
    OSD::Map(HashMap::from([
        (
            "joint_names".to_owned(),
            OSD::Array(
                [
                    OSD::String("mPelvis".to_owned()),
                    OSD::String("mTorso".to_owned()),
                ]
                .into(),
            ),
        ),
        (
            "inverse_bind_matrix".to_owned(),
            OSD::Array(vec![identity(), identity()]),
        ),
        (
            "alt_inverse_bind_matrix".to_owned(),
            OSD::Array(vec![identity(), identity()]),
        ),
        ("bind_shape_matrix".to_owned(), identity()),
        ("pelvis_offset".to_owned(), OSD::Real(0.25)),
        (
            "lock_scale_if_joint_position".to_owned(),
            OSD::Boolean(true),
        ),
    ]))
}

fn weights() -> Vec<u8> {
    let quarter = (0.25_f32 * 65_535.0).round() as u16;
    let three_quarters = (0.75_f32 * 65_535.0).round() as u16;
    let half = (0.5_f32 * 65_535.0).round() as u16;
    let mut bytes = vec![0];
    bytes.extend(quarter.to_le_bytes());
    bytes.push(1);
    bytes.extend(three_quarters.to_le_bytes());
    bytes.push(0xff);
    bytes.push(0);
    bytes.extend(half.to_le_bytes());
    bytes.push(1);
    bytes.extend(half.to_le_bytes());
    bytes.push(0xff);
    bytes.push(0xff);
    bytes
}

fn rigged_asset(triangle: [u16; 3], weight_bytes: Vec<u8>) -> Vec<u8> {
    pack(BTreeMap::from([
        (
            "high_lod",
            OSD::Array(vec![rigged_submesh(triangle, weight_bytes)]),
        ),
        ("skin", skin()),
    ]))
}

fn mesh_hash(mesh: &SimpleMesh) -> u64 {
    let mut hash = 0xcbf2_9ce4_8422_2325_u64;
    for vertex in &mesh.vertices {
        for value in [
            vertex.position.x,
            vertex.position.y,
            vertex.position.z,
            vertex.normal.x,
            vertex.normal.y,
            vertex.normal.z,
            vertex.tex_coord.x,
            vertex.tex_coord.y,
        ] {
            hash ^= u64::from(value.to_bits());
            hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
        }
    }
    for index in &mesh.indices {
        hash ^= u64::from(*index);
        hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
    }
    hash
}

#[test]
fn model_workflow_asset_round_trips_and_falls_back_deterministically() {
    let mut model = ModelPrim::new().expect("model prim");
    let mut face = ModelFace::new().expect("model face");
    face.vertices = vec![
        Vertex {
            position: Vector3 {
                x: -0.5,
                y: -0.5,
                z: 0.0,
            },
            normal: Vector3::unit_z(),
            tex_coord: Vector2::zero(),
        },
        Vertex {
            position: Vector3 {
                x: 0.5,
                y: -0.5,
                z: 0.0,
            },
            normal: Vector3::unit_z(),
            tex_coord: Vector2 { x: 1.0, y: 0.0 },
        },
        Vertex {
            position: Vector3 {
                x: -0.5,
                y: 0.5,
                z: 0.0,
            },
            normal: Vector3::unit_z(),
            tex_coord: Vector2 { x: 0.0, y: 1.0 },
        },
    ];
    face.indices = vec![0, 1, 2];
    model.faces.push(face);
    model.create_asset(UUID::zero()).expect("mesh asset");
    let renderer = MeshFoundry::new().expect("renderer");
    let primitive = Primitive::new_with_constructor().expect("primitive");

    let highest = renderer
        .generate_faceted_mesh_mesh_with_primitive_bytes_detail_level(
            primitive.clone(),
            model.asset.clone(),
            DetailLevel::Highest,
        )
        .expect("highest decode")
        .expect("highest mesh");
    let fallback = renderer
        .generate_faceted_mesh_mesh_with_primitive_bytes_detail_level(
            primitive,
            model.asset.clone(),
            DetailLevel::Low,
        )
        .expect("fallback decode")
        .expect("fallback mesh");
    assert_eq!(highest.faces.len(), 1);
    assert_eq!(highest.faces[0].vertices.len(), 3);
    assert_eq!(highest.faces[0].indices, [0, 1, 2]);
    assert_eq!(fallback.faces[0].indices, highest.faces[0].indices);
    let aux = highest.faces[0]
        .user_data
        .downcast_ref::<MeshFaceAux>()
        .expect("generated tangent domain");
    assert_eq!(aux.tangents.len(), 3);
    assert!(aux.tangents.iter().all(|tangent| tangent.w.abs() == 1.0));

    let unpacked = renderer
        .unpack_mesh(model.asset)
        .expect("unpack call")
        .expect("unpacked map");
    assert!(matches!(unpacked.get("high_lod"), Some(OSD::Array(_))));
    assert!(matches!(unpacked.get("physics_convex"), Some(OSD::Map(_))));
}

#[test]
fn rigged_mesh_preserves_skin_uv_tangent_material_and_ordering_domains() {
    let texture =
        UUID::new_with_string("11111111-2222-3333-4444-555555555555".into()).expect("texture UUID");
    let material = UUID::new_with_string("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee".into())
        .expect("material UUID");
    let mut textures = PrimitiveTextureEntry::new_with_uuid(texture).expect("texture entry");
    textures
        .create_face(0)
        .expect("face override")
        .set_render_material_id(material);
    let mut primitive = Primitive::new_with_constructor().expect("primitive");
    primitive.textures = Some(textures);
    let renderer = MeshFoundry::new().expect("renderer");
    let mesh = renderer
        .generate_faceted_mesh_mesh_with_primitive_bytes(
            primitive,
            rigged_asset([0, 1, 2], weights()),
        )
        .expect("rigged decode")
        .expect("rigged mesh");

    let skin = mesh.skin_data.as_ref().expect("skin data");
    assert_eq!(skin.joint_names, ["mPelvis", "mTorso"]);
    assert_eq!(skin.inverse_bind_matrices.len(), 32);
    assert_eq!(skin.alt_inverse_bind_matrices.len(), 32);
    assert_eq!(skin.bind_shape_matrix.len(), 16);
    assert_eq!(skin.pelvis_offset, 0.25);
    assert!(skin.lock_scale_if_joint_position);
    assert_eq!(
        RiggedSkinMath::build_inv_bind_matrices(skin.clone())
            .expect("decoded inverse bind matrices")
            .len(),
        2
    );
    assert_eq!(
        RiggedSkinMath::extract_joint_position_overrides(skin.clone())
            .expect("decoded joint overrides")
            .len(),
        2
    );
    let face = &mesh.faces[0];
    assert_eq!(face.indices, [0, 1, 2]);
    assert_eq!(
        face.normalized_scale,
        Vector3 {
            x: 2.0,
            y: 3.0,
            z: 4.0
        }
    );
    assert_eq!(face.texture_face.texture_id(), texture);
    assert_eq!(face.texture_face.render_material_id(), material);
    assert_eq!(face.tex_coords1.as_ref().expect("UV1").len(), 3);
    let weights = face.weights.as_ref().expect("weights");
    assert_eq!(weights.len(), 3);
    for weight in weights {
        let total = weight.weight0 + weight.weight1 + weight.weight2 + weight.weight3;
        assert!((total - 1.0).abs() < 1.0e-6);
    }
    assert_eq!(weights[0].joint0, 0);
    assert_eq!(weights[0].joint1, 1);
    assert_eq!(weights[2].weight0, 1.0);
    let tangents = &face
        .user_data
        .downcast_ref::<MeshFaceAux>()
        .expect("stored tangents")
        .tangents;
    assert_eq!(tangents.len(), 3);
    assert!(
        tangents
            .iter()
            .all(|tangent| tangent.x > 0.999 && tangent.w == -1.0)
    );
}

#[test]
fn compressed_submesh_decode_has_stable_golden_output() {
    let section = compress(OSD::Array(vec![rigged_submesh([0, 1, 2], Vec::new())]));
    let renderer = MeshFoundry::new().expect("renderer");
    let mesh = renderer
        .mesh_sub_mesh_as_simple_mesh(
            Primitive::new_with_constructor().expect("primitive"),
            section,
        )
        .expect("submesh decode")
        .expect("simple mesh");
    assert_eq!(mesh_hash(&mesh), 0xcb32_07be_6dea_73c8);
}

#[test]
fn compressed_submesh_decode_rebases_indices_in_face_order() {
    let section = compress(OSD::Array(vec![
        rigged_submesh([0, 1, 2], Vec::new()),
        rigged_submesh([2, 1, 0], Vec::new()),
    ]));
    let mesh = MeshFoundry::new()
        .expect("renderer")
        .mesh_sub_mesh_as_simple_mesh(
            Primitive::new_with_constructor().expect("primitive"),
            section,
        )
        .expect("submesh decode")
        .expect("simple mesh");
    assert_eq!(mesh.vertices.len(), 6);
    assert_eq!(mesh.indices, [0, 1, 2, 5, 4, 3]);
}

#[test]
fn convex_hulls_decode_bounding_and_decomposed_domains() {
    let positions = quantized3(
        &[[-0.5, -0.5, -0.5], [0.5, -0.5, -0.5], [-0.5, 0.5, 0.5]],
        [-0.5; 3],
        [0.5; 3],
    );
    let section = compress(OSD::Map(HashMap::from([
        (
            "Min".to_owned(),
            OSD::from_vector3(Vector3 {
                x: -0.5,
                y: -0.5,
                z: -0.5,
            })
            .expect("min"),
        ),
        (
            "Max".to_owned(),
            OSD::from_vector3(Vector3 {
                x: 0.5,
                y: 0.5,
                z: 0.5,
            })
            .expect("max"),
        ),
        ("BoundingVerts".to_owned(), OSD::Binary(positions.clone())),
        ("HullList".to_owned(), OSD::Binary(vec![3])),
        ("Positions".to_owned(), OSD::Binary(positions)),
    ])));
    let renderer = MeshFoundry::new().expect("renderer");
    let primitive = Primitive::new_with_constructor().expect("primitive");
    let mut bounding = Vec::new();
    let hulls = renderer
        .mesh_sub_mesh_as_convex_hulls_with_primitive_bytes_list(
            primitive.clone(),
            section.clone(),
            &mut bounding,
        )
        .expect("convex decode");
    assert_eq!(bounding.len(), 3);
    assert_eq!(hulls.len(), 1);
    assert_eq!(hulls[0], bounding);
    assert_eq!(
        renderer
            .mesh_sub_mesh_as_convex_hulls_with_primitive_bytes(primitive, section)
            .expect("convex overload"),
        hulls
    );
}

#[test]
fn malformed_offsets_indices_and_weights_return_source_context() {
    let source =
        UUID::new_with_string("12345678-1234-5678-9abc-def012345678".into()).expect("source UUID");
    let mut primitive = Primitive::new_with_constructor().expect("primitive");
    primitive.id = source;
    let renderer = MeshFoundry::new().expect("renderer");
    let invalid_index = renderer.generate_faceted_mesh_mesh_with_primitive_bytes(
        primitive.clone(),
        rigged_asset([0, 1, 3], weights()),
    );
    assert!(matches!(
        invalid_index,
        Err(Error::Rendering { source: actual, context: "mesh triangle index is out of range" })
            if actual == source
    ));

    let invalid_weights = renderer.generate_faceted_mesh_mesh_with_primitive_bytes(
        primitive.clone(),
        rigged_asset([0, 1, 2], vec![2, 0, 128, 0xff, 0xff, 0xff]),
    );
    assert!(matches!(
        invalid_weights,
        Err(Error::Rendering { source: actual, context: "mesh skin joint index is out of range" })
            if actual == source
    ));

    let header = OSD::Map(HashMap::from([(
        "high_lod".to_owned(),
        OSD::Map(HashMap::from([
            ("offset".to_owned(), OSD::Integer(i32::MAX)),
            ("size".to_owned(), OSD::Integer(64)),
        ])),
    )]));
    let invalid_offset = OSDParser::serialize_llsd_binary_with_osd_boolean(header, false)
        .expect("invalid fixture header");
    assert!(matches!(
        renderer.generate_faceted_mesh_mesh_with_primitive_bytes(primitive, invalid_offset),
        Err(Error::Rendering { source: actual, context: "mesh section is outside the asset" })
            if actual == source
    ));
}

#[test]
fn mesh_sculpt_mirror_and_invert_update_coordinates_normals_and_tangents() {
    let mut primitive = Primitive::new_with_constructor().expect("primitive");
    let mut bytes = UUID::zero().get_bytes().expect("UUID bytes");
    bytes.push(SculptType::Mesh as u8 | SculptType::Mirror as u8 | SculptType::Invert as u8);
    primitive.sculpt =
        Some(PrimitiveSculptData::new_with_bytes_int32(bytes, 0).expect("sculpt flags"));
    let mesh = MeshFoundry::new()
        .expect("renderer")
        .generate_faceted_mesh_mesh_with_primitive_bytes(
            primitive,
            rigged_asset([0, 1, 2], weights()),
        )
        .expect("modified decode")
        .expect("modified mesh");
    let face = &mesh.faces[0];
    // Mirror and invert each reverse winding, so applying both preserves it.
    assert_eq!(face.indices, [0, 1, 2]);
    assert_eq!(face.vertices[0].position.x, 0.5);
    assert!(face.vertices[0].normal.z < -0.999);
    let tangents = &face
        .user_data
        .downcast_ref::<MeshFaceAux>()
        .expect("tangents")
        .tangents;
    assert!(tangents[0].x < -0.999);
    assert_eq!(tangents[0].w, -1.0);
}
