//! Offline Collada conversion, deterministic mesh assets, and opt-in uploads.

// Owned DTO parameters and the public model shapes follow the fixed mapped API;
// format bounds make the intentional numeric narrowing safe.
#![allow(clippy::pedantic)]

use std::collections::HashMap;
use std::fs;
use std::io::Write;
use std::path::{Component, Path, PathBuf};

use flate2::{Compression, write::ZlibEncoder};
use libremetaverse_imaging::ITextureCodec;
use libremetaverse_structured_data::{OSD, OSDFormat, OSDParser};
use libremetaverse_types::compat::{CancellationToken, Uri};
use libremetaverse_types::{AssetType, Color4, Error, Quaternion, UUID, Vector2, Vector3};

use crate::GridClient;
use crate::rendering::Vertex;

const MAX_COLLADA_BYTES: usize = 64 * 1024 * 1024;
const MAX_IMAGE_BYTES: usize = 64 * 1024 * 1024;
const MAX_VERTICES: usize = 1_000_000;
const MAX_FACES: usize = 100_000;

fn model_error(context: &'static str) -> Error {
    Error::Parse {
        position: 0,
        context,
    }
}

pub struct ModelMaterial {
    pub diffuse_color: Color4,
    pub id: String,
    pub texture: String,
    pub texture_data: Vec<u8>,
}

impl Clone for ModelMaterial {
    fn clone(&self) -> Self {
        Self {
            diffuse_color: self.diffuse_color,
            id: self.id.clone(),
            texture: self.texture.clone(),
            texture_data: self.texture_data.clone(),
        }
    }
}

impl std::fmt::Debug for ModelMaterial {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("ModelMaterial")
            .field("id", &self.id)
            .field("texture", &self.texture)
            .field("texture_bytes", &self.texture_data.len())
            .finish()
    }
}

impl ModelMaterial {
    pub fn new() -> Result<Self, Error> {
        Ok(Self {
            diffuse_color: Color4::white(),
            id: String::new(),
            texture: String::new(),
            texture_data: Vec::new(),
        })
    }
}

pub struct ModelFace {
    pub indices: Vec<u32>,
    pub material: ModelMaterial,
    pub material_id: String,
    pub vertices: Vec<Vertex>,
}

impl std::fmt::Debug for ModelFace {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("ModelFace")
            .field("indices", &self.indices)
            .field("material", &self.material)
            .field("material_id", &self.material_id)
            .field("vertex_count", &self.vertices.len())
            .finish()
    }
}

impl ModelFace {
    pub fn new() -> Result<Self, Error> {
        Ok(Self {
            indices: Vec::new(),
            material: ModelMaterial::new()?,
            material_id: String::new(),
            vertices: Vec::new(),
        })
    }

    pub fn add_vertex(&mut self, vertex: Vertex) -> Result<(), Error> {
        if self.indices.len() >= MAX_VERTICES * 3 {
            return Err(Error::Argument);
        }
        let index = self
            .vertices
            .iter()
            .position(|candidate| vertex_equal(candidate, &vertex));
        let index = match index {
            Some(index) => index,
            None => {
                if self.vertices.len() >= MAX_VERTICES {
                    return Err(Error::Argument);
                }
                self.vertices.push(vertex);
                self.vertices.len() - 1
            }
        };
        self.indices
            .push(u32::try_from(index).map_err(|_| Error::Argument)?);
        Ok(())
    }
}

fn vertex_equal(left: &Vertex, right: &Vertex) -> bool {
    left.position.x.to_bits() == right.position.x.to_bits()
        && left.position.y.to_bits() == right.position.y.to_bits()
        && left.position.z.to_bits() == right.position.z.to_bits()
        && left.normal.x.to_bits() == right.normal.x.to_bits()
        && left.normal.y.to_bits() == right.normal.y.to_bits()
        && left.normal.z.to_bits() == right.normal.z.to_bits()
        && left.tex_coord.x.to_bits() == right.tex_coord.x.to_bits()
        && left.tex_coord.y.to_bits() == right.tex_coord.y.to_bits()
}

#[derive(Debug)]
pub struct ModelPrim {
    pub asset: Vec<u8>,
    pub bound_max: Vector3,
    pub bound_min: Vector3,
    pub faces: Vec<ModelFace>,
    pub id: String,
    pub position: Vector3,
    pub positions: Vec<Vector3>,
    pub rotation: Quaternion,
    pub scale: Vector3,
}

impl ModelPrim {
    pub fn new() -> Result<Self, Error> {
        Ok(Self {
            asset: Vec::new(),
            bound_max: Vector3 {
                x: f32::MIN,
                y: f32::MIN,
                z: f32::MIN,
            },
            bound_min: Vector3 {
                x: f32::MAX,
                y: f32::MAX,
                z: f32::MAX,
            },
            faces: Vec::new(),
            id: String::new(),
            position: Vector3::zero(),
            positions: Vec::new(),
            rotation: Quaternion::identity(),
            scale: Vector3::one(),
        })
    }

    pub fn physics_stub() -> Result<OSD, Error> {
        Ok(OSD::Map(HashMap::from([
            (
                "Max".to_owned(),
                OSD::from_vector3(Vector3 {
                    x: 0.5,
                    y: 0.5,
                    z: 0.5,
                })?,
            ),
            (
                "Min".to_owned(),
                OSD::from_vector3(Vector3 {
                    x: -0.5,
                    y: -0.5,
                    z: -0.5,
                })?,
            ),
            (
                "BoundingVerts".to_owned(),
                OSD::Binary(vec![
                    255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 127, 0, 0, 255, 255, 255,
                    127, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 255, 255, 0,
                    0, 255, 127, 255, 255, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 255, 255,
                    0, 0, 0, 0, 255, 255, 0, 0, 255, 255,
                ]),
            ),
        ])))
    }

    pub fn create_asset(&mut self, creator: UUID) -> Result<(), Error> {
        if self.faces.len() > MAX_FACES {
            return Err(Error::Argument);
        }
        let mut face_values = Vec::with_capacity(self.faces.len());
        for face in &self.faces {
            if face.vertices.len() > u16::MAX as usize
                || face.indices.iter().any(|index| *index > u16::MAX as u32)
            {
                return Err(Error::Argument);
            }
            let (uv_min, uv_max) = uv_bounds(&face.vertices);
            let mut positions = Vec::with_capacity(face.vertices.len() * 6);
            let mut normals = Vec::with_capacity(face.vertices.len() * 6);
            let mut tex_coords = Vec::with_capacity(face.vertices.len() * 4);
            for vertex in &face.vertices {
                for value in [vertex.position.x, vertex.position.y, vertex.position.z] {
                    positions.extend_from_slice(&quantize(value, -0.5, 0.5).to_le_bytes());
                }
                for value in [vertex.normal.x, vertex.normal.y, vertex.normal.z] {
                    normals.extend_from_slice(&quantize(value, -1.0, 1.0).to_le_bytes());
                }
                tex_coords.extend_from_slice(
                    &quantize(vertex.tex_coord.x, uv_min.x, uv_max.x).to_le_bytes(),
                );
                tex_coords.extend_from_slice(
                    &quantize(vertex.tex_coord.y, uv_min.y, uv_max.y).to_le_bytes(),
                );
            }
            let mut triangles = Vec::with_capacity(face.indices.len() * 2);
            for index in &face.indices {
                triangles.extend_from_slice(&(*index as u16).to_le_bytes());
            }
            face_values.push(OSD::Map(HashMap::from([
                (
                    "TexCoord0Domain".to_owned(),
                    OSD::Map(HashMap::from([
                        ("Min".to_owned(), OSD::from_vector2(uv_min)?),
                        ("Max".to_owned(), OSD::from_vector2(uv_max)?),
                    ])),
                ),
                (
                    "PositionDomain".to_owned(),
                    OSD::Map(HashMap::from([
                        (
                            "Min".to_owned(),
                            OSD::from_vector3(Vector3 {
                                x: -0.5,
                                y: -0.5,
                                z: -0.5,
                            })?,
                        ),
                        (
                            "Max".to_owned(),
                            OSD::from_vector3(Vector3 {
                                x: 0.5,
                                y: 0.5,
                                z: 0.5,
                            })?,
                        ),
                    ])),
                ),
                ("Position".to_owned(), OSD::Binary(positions)),
                ("Normal".to_owned(), OSD::Binary(normals)),
                ("TexCoord0".to_owned(), OSD::Binary(tex_coords)),
                ("TriangleList".to_owned(), OSD::Binary(triangles)),
            ])));
        }
        let mesh = compress_osd(OSD::Array(face_values))?;
        let physics = compress_osd(Self::physics_stub()?)?;
        let header = OSD::Map(HashMap::from([
            ("version".to_owned(), OSD::Integer(1)),
            ("creator".to_owned(), OSD::UUID(creator)),
            ("date".to_owned(), OSD::Date(std::time::UNIX_EPOCH)),
            (
                "high_lod".to_owned(),
                OSD::Map(HashMap::from([
                    ("offset".to_owned(), OSD::Integer(0)),
                    (
                        "size".to_owned(),
                        OSD::Integer(i32::try_from(mesh.len()).map_err(|_| Error::Argument)?),
                    ),
                ])),
            ),
            (
                "physics_convex".to_owned(),
                OSD::Map(HashMap::from([
                    (
                        "offset".to_owned(),
                        OSD::Integer(i32::try_from(mesh.len()).map_err(|_| Error::Argument)?),
                    ),
                    (
                        "size".to_owned(),
                        OSD::Integer(i32::try_from(physics.len()).map_err(|_| Error::Argument)?),
                    ),
                ])),
            ),
        ]));
        let header = OSDParser::serialize_llsd_binary_with_osd_boolean(header, false)?;
        let total = header
            .len()
            .checked_add(mesh.len())
            .and_then(|n| n.checked_add(physics.len()))
            .ok_or(Error::Argument)?;
        if total > MAX_COLLADA_BYTES {
            return Err(Error::Argument);
        }
        self.asset = Vec::with_capacity(total);
        self.asset.extend_from_slice(&header);
        self.asset.extend_from_slice(&mesh);
        self.asset.extend_from_slice(&physics);
        Ok(())
    }
}

fn uv_bounds(vertices: &[Vertex]) -> (Vector2, Vector2) {
    if vertices.is_empty() {
        return (Vector2::zero(), Vector2::zero());
    }
    let mut min = Vector2 {
        x: f32::MAX,
        y: f32::MAX,
    };
    let mut max = Vector2 {
        x: f32::MIN,
        y: f32::MIN,
    };
    for vertex in vertices {
        min.x = min.x.min(vertex.tex_coord.x);
        min.y = min.y.min(vertex.tex_coord.y);
        max.x = max.x.max(vertex.tex_coord.x);
        max.y = max.y.max(vertex.tex_coord.y);
    }
    (min, max)
}
fn quantize(value: f32, min: f32, max: f32) -> u16 {
    if !value.is_finite() || !min.is_finite() || !max.is_finite() || max <= min {
        0
    } else {
        (((value - min) / (max - min)).clamp(0.0, 1.0) * 65535.0).round() as u16
    }
}
fn compress_osd(value: OSD) -> Result<Vec<u8>, Error> {
    let bytes = OSDParser::serialize_llsd_binary_with_osd_boolean(value, false)?;
    let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
    encoder
        .write_all(&bytes)
        .map_err(|_| Error::InvalidOperation)?;
    encoder.finish().map_err(|_| Error::InvalidOperation)
}

pub struct ColladaLoader {
    texture_codec: Option<Box<dyn ITextureCodec>>,
}
impl std::fmt::Debug for ColladaLoader {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("ColladaLoader")
            .field("has_texture_codec", &self.texture_codec.is_some())
            .finish()
    }
}

impl ColladaLoader {
    pub fn new(texture_codec: Option<Box<dyn ITextureCodec>>) -> Result<Self, Error> {
        Ok(Self { texture_codec })
    }
    pub fn load(&self, filename: String, load_images: bool) -> Result<Vec<ModelPrim>, Error> {
        let path = PathBuf::from(filename);
        let metadata = fs::metadata(&path).map_err(|_| Error::InvalidOperation)?;
        if metadata.len() as usize > MAX_COLLADA_BYTES {
            return Err(Error::Argument);
        }
        let xml = fs::read_to_string(&path).map_err(|_| model_error("Collada UTF-8"))?;
        let document = roxmltree::Document::parse(&xml).map_err(|_| model_error("Collada XML"))?;
        if document.descendants().count() > 1_000_000 {
            return Err(Error::Argument);
        }
        let root = path
            .parent()
            .unwrap_or_else(|| Path::new("."))
            .canonicalize()
            .map_err(|_| Error::InvalidOperation)?;
        let materials =
            parse_materials(&document, &root, load_images, self.texture_codec.as_deref())?;
        let material_bindings: HashMap<_, _> = document
            .descendants()
            .filter(|node| node.has_tag_name("instance_material"))
            .filter_map(|node| {
                Some((
                    node.attribute("symbol")?.to_owned(),
                    strip_hash(node.attribute("target")?).to_owned(),
                ))
            })
            .collect();
        let transform = AssetTransform::from_document(&document)?;
        let mut prims = Vec::new();
        for geometry in document
            .descendants()
            .filter(|node| node.has_tag_name("geometry"))
        {
            if prims.len() >= MAX_FACES {
                return Err(Error::Argument);
            }
            let Some(mesh) = geometry.children().find(|node| node.has_tag_name("mesh")) else {
                continue;
            };
            let mut sources = HashMap::new();
            for source in mesh.children().filter(|node| node.has_tag_name("source")) {
                if let Some(id) = source.attribute("id") {
                    sources.insert(id.to_owned(), parse_source(source)?);
                }
            }
            let mut vertices_sources = HashMap::new();
            for vertices in mesh.children().filter(|node| node.has_tag_name("vertices")) {
                if let Some(id) = vertices.attribute("id")
                    && let Some(input) = vertices.children().find(|n| {
                        n.has_tag_name("input") && n.attribute("semantic") == Some("POSITION")
                    })
                {
                    vertices_sources.insert(
                        id.to_owned(),
                        strip_hash(input.attribute("source").unwrap_or_default()).to_owned(),
                    );
                }
            }
            let Some(position_source) = vertices_sources
                .values()
                .next()
                .and_then(|id| sources.get(id))
            else {
                return Err(model_error("Collada position source"));
            };
            let mut prim = ModelPrim::new()?;
            prim.id = geometry.attribute("id").unwrap_or_default().to_owned();
            prim.positions = position_source.vectors3()?;
            for position in &mut prim.positions {
                *position = transform.position(*position);
            }
            normalize_positions(&mut prim)?;
            for primitive in mesh
                .children()
                .filter(|n| n.has_tag_name("triangles") || n.has_tag_name("polylist"))
            {
                prim.faces.push(parse_faces(
                    primitive,
                    &sources,
                    &vertices_sources,
                    &prim.positions,
                    primitive
                        .attribute("material")
                        .and_then(|symbol| {
                            material_bindings
                                .get(symbol)
                                .and_then(|target| materials.get(target))
                                .or_else(|| materials.get(symbol))
                        })
                        .cloned(),
                    transform,
                )?);
            }
            if prim.faces.is_empty() {
                continue;
            }
            prim.create_asset(UUID::zero())?;
            prims.push(prim);
        }
        Ok(prims)
    }
}

#[derive(Clone, Copy)]
enum UpAxis {
    X,
    Y,
    Z,
}

#[derive(Clone, Copy)]
struct AssetTransform {
    unit: f32,
    up: UpAxis,
}

impl AssetTransform {
    fn from_document(document: &roxmltree::Document<'_>) -> Result<Self, Error> {
        let asset = document
            .descendants()
            .find(|node| node.has_tag_name("asset"));
        let unit = asset
            .and_then(|node| node.descendants().find(|child| child.has_tag_name("unit")))
            .and_then(|node| node.attribute("meter"))
            .map(str::parse::<f32>)
            .transpose()
            .map_err(|_| model_error("Collada unit"))?
            .unwrap_or(1.0);
        if !unit.is_finite() || unit <= 0.0 || unit > 1_000_000.0 {
            return Err(model_error("Collada unit bounds"));
        }
        let up = match asset
            .and_then(|node| {
                node.descendants()
                    .find(|child| child.has_tag_name("up_axis"))
            })
            .and_then(|node| node.text())
            .map(str::trim)
            .unwrap_or("Y_UP")
        {
            "X_UP" => UpAxis::X,
            "Y_UP" => UpAxis::Y,
            "Z_UP" => UpAxis::Z,
            _ => return Err(model_error("Collada up axis")),
        };
        Ok(Self { unit, up })
    }

    fn axis(self, value: Vector3) -> Vector3 {
        match self.up {
            UpAxis::X => Vector3 {
                x: -value.z,
                y: value.y,
                z: value.x,
            },
            UpAxis::Y => Vector3 {
                x: value.x,
                y: -value.z,
                z: value.y,
            },
            UpAxis::Z => value,
        }
    }

    fn position(self, value: Vector3) -> Vector3 {
        let value = self.axis(value);
        Vector3 {
            x: value.x * self.unit,
            y: value.y * self.unit,
            z: value.z * self.unit,
        }
    }
}

#[derive(Debug)]
struct Source {
    values: Vec<f32>,
    stride: usize,
}
impl Source {
    fn vectors3(&self) -> Result<Vec<Vector3>, Error> {
        if self.stride < 3 {
            return Err(model_error("Collada VEC3 stride"));
        }
        self.values
            .chunks(self.stride)
            .map(|v| {
                if v.len() >= 3 {
                    Ok(Vector3 {
                        x: v[0],
                        y: v[1],
                        z: v[2],
                    })
                } else {
                    Err(model_error("Collada VEC3 data"))
                }
            })
            .collect()
    }
}
fn parse_source(node: roxmltree::Node<'_, '_>) -> Result<Source, Error> {
    let array = node
        .descendants()
        .find(|n| n.has_tag_name("float_array"))
        .ok_or_else(|| model_error("Collada float array"))?;
    let values = parse_floats(array.text().unwrap_or_default())?;
    if values.len() > MAX_VERTICES * 4 {
        return Err(Error::Argument);
    }
    let stride = node
        .descendants()
        .find(|n| n.has_tag_name("accessor"))
        .and_then(|n| n.attribute("stride"))
        .and_then(|v| v.parse().ok())
        .unwrap_or(1);
    if stride == 0 || stride > 16 || values.len() % stride != 0 {
        return Err(model_error("Collada source stride"));
    }
    Ok(Source { values, stride })
}
fn parse_floats(text: &str) -> Result<Vec<f32>, Error> {
    text.split_whitespace()
        .map(|v| {
            v.parse::<f32>()
                .ok()
                .filter(|n| n.is_finite())
                .ok_or_else(|| model_error("Collada finite float"))
        })
        .collect()
}
fn parse_ints(text: &str) -> Result<Vec<usize>, Error> {
    text.split_whitespace()
        .map(|v| v.parse::<usize>().map_err(|_| model_error("Collada index")))
        .collect()
}
fn strip_hash(value: &str) -> &str {
    value.strip_prefix('#').unwrap_or(value)
}

fn normalize_positions(prim: &mut ModelPrim) -> Result<(), Error> {
    if prim.positions.is_empty() || prim.positions.len() > MAX_VERTICES {
        return Err(model_error("Collada positions"));
    }
    for p in &prim.positions {
        prim.bound_min.x = prim.bound_min.x.min(p.x);
        prim.bound_min.y = prim.bound_min.y.min(p.y);
        prim.bound_min.z = prim.bound_min.z.min(p.z);
        prim.bound_max.x = prim.bound_max.x.max(p.x);
        prim.bound_max.y = prim.bound_max.y.max(p.y);
        prim.bound_max.z = prim.bound_max.z.max(p.z);
    }
    let scale = Vector3 {
        x: prim.bound_max.x - prim.bound_min.x,
        y: prim.bound_max.y - prim.bound_min.y,
        z: prim.bound_max.z - prim.bound_min.z,
    };
    prim.scale = scale;
    prim.position = Vector3 {
        x: prim.bound_min.x + scale.x / 2.0,
        y: prim.bound_min.y + scale.y / 2.0,
        z: prim.bound_min.z + scale.z / 2.0,
    };
    for p in &mut prim.positions {
        p.x = if scale.x == 0.0 {
            0.0
        } else {
            (p.x - prim.bound_min.x) / scale.x - 0.5
        };
        p.y = if scale.y == 0.0 {
            0.0
        } else {
            (p.y - prim.bound_min.y) / scale.y - 0.5
        };
        p.z = if scale.z == 0.0 {
            0.0
        } else {
            (p.z - prim.bound_min.z) / scale.z - 0.5
        };
    }
    Ok(())
}

fn parse_faces(
    node: roxmltree::Node<'_, '_>,
    sources: &HashMap<String, Source>,
    vertices_sources: &HashMap<String, String>,
    positions: &[Vector3],
    material: Option<ModelMaterial>,
    transform: AssetTransform,
) -> Result<ModelFace, Error> {
    let mut inputs = Vec::new();
    let mut stride = 0usize;
    for input in node.children().filter(|n| n.has_tag_name("input")) {
        let offset = input
            .attribute("offset")
            .unwrap_or("0")
            .parse::<usize>()
            .map_err(|_| model_error("Collada input offset"))?;
        stride = stride.max(offset + 1);
        inputs.push((
            input.attribute("semantic").unwrap_or_default(),
            offset,
            strip_hash(input.attribute("source").unwrap_or_default()),
        ));
    }
    if stride == 0 || stride > 32 {
        return Err(model_error("Collada primitive stride"));
    }
    let indices = parse_ints(
        node.children()
            .find(|n| n.has_tag_name("p"))
            .and_then(|n| n.text())
            .unwrap_or_default(),
    )?;
    let counts = if node.has_tag_name("triangles") {
        vec![
            3usize;
            node.attribute("count")
                .and_then(|v| v.parse().ok())
                .unwrap_or(indices.len() / (stride * 3))
        ]
    } else {
        parse_ints(
            node.children()
                .find(|n| n.has_tag_name("vcount"))
                .and_then(|n| n.text())
                .unwrap_or_default(),
        )?
    };
    let mut face = ModelFace::new()?;
    face.material_id = node.attribute("material").unwrap_or_default().to_owned();
    if let Some(material) = material {
        face.material = material
    }
    let mut cursor = 0usize;
    for count in counts {
        if !(3..=4).contains(&count) {
            return Err(model_error("Collada supports triangles and quads"));
        }
        let mut polygon = Vec::with_capacity(count);
        for vertex in 0..count {
            let base = cursor.checked_add(vertex * stride).ok_or(Error::Argument)?;
            let mut out = Vertex {
                position: Vector3::zero(),
                normal: Vector3::zero(),
                tex_coord: Vector2::zero(),
            };
            for (semantic, offset, source_id) in &inputs {
                let index = *indices
                    .get(base + offset)
                    .ok_or_else(|| model_error("Collada index bounds"))?;
                match *semantic {
                    "VERTEX" => {
                        let position_id = vertices_sources
                            .get(*source_id)
                            .ok_or_else(|| model_error("Collada vertices source"))?;
                        let _ = position_id;
                        out.position = *positions
                            .get(index)
                            .ok_or_else(|| model_error("Collada position index"))?
                    }
                    "NORMAL" => {
                        let source = sources
                            .get(*source_id)
                            .ok_or_else(|| model_error("Collada normal source"))?;
                        let at = index * source.stride;
                        if at + 2 >= source.values.len() {
                            return Err(model_error("Collada normal index"));
                        }
                        out.normal = transform.axis(Vector3 {
                            x: source.values[at],
                            y: source.values[at + 1],
                            z: source.values[at + 2],
                        })
                    }
                    "TEXCOORD" => {
                        let source = sources
                            .get(*source_id)
                            .ok_or_else(|| model_error("Collada UV source"))?;
                        let at = index * source.stride;
                        if at + 1 >= source.values.len() {
                            return Err(model_error("Collada UV index"));
                        }
                        out.tex_coord = Vector2 {
                            x: source.values[at],
                            y: source.values[at + 1],
                        }
                    }
                    _ => {}
                }
            }
            polygon.push(out);
        }
        cursor = cursor.checked_add(count * stride).ok_or(Error::Argument)?;
        let order: &[usize] = if count == 3 {
            &[0, 1, 2]
        } else {
            &[0, 1, 2, 0, 2, 3]
        };
        for index in order {
            let vertex = polygon
                .get(*index)
                .ok_or_else(|| model_error("Collada polygon index"))?;
            face.add_vertex(Vertex {
                position: vertex.position,
                normal: vertex.normal,
                tex_coord: vertex.tex_coord,
            })?;
        }
    }
    Ok(face)
}

fn parse_materials(
    document: &roxmltree::Document<'_>,
    root: &Path,
    load_images: bool,
    codec: Option<&dyn ITextureCodec>,
) -> Result<HashMap<String, ModelMaterial>, Error> {
    let mut images = HashMap::new();
    for image in document.descendants().filter(|n| n.has_tag_name("image")) {
        if let (Some(id), Some(init)) = (
            image.attribute("id"),
            image
                .descendants()
                .find(|n| n.has_tag_name("init_from"))
                .and_then(|n| n.text()),
        ) {
            images.insert(id.to_owned(), init.to_owned());
        }
    }
    let mut effects = HashMap::new();
    for effect in document.descendants().filter(|n| n.has_tag_name("effect")) {
        let Some(effect_id) = effect.attribute("id") else {
            continue;
        };
        let mut value = ModelMaterial::new()?;
        value.id = effect_id.to_owned();
        let diffuse = effect
            .descendants()
            .find(|node| node.has_tag_name("diffuse"));
        if let Some(color) = diffuse
            .and_then(|node| node.descendants().find(|child| child.has_tag_name("color")))
            .and_then(|node| node.text())
        {
            apply_diffuse_color(&mut value, color)?;
        }
        if let Some(sampler) = diffuse
            .and_then(|node| {
                node.descendants()
                    .find(|child| child.has_tag_name("texture"))
            })
            .and_then(|node| node.attribute("texture"))
        {
            let surface = effect
                .descendants()
                .find(|node| {
                    node.has_tag_name("newparam") && node.attribute("sid") == Some(sampler)
                })
                .and_then(|node| {
                    node.descendants()
                        .find(|child| child.has_tag_name("sampler2D"))
                })
                .and_then(|node| {
                    node.descendants()
                        .find(|child| child.has_tag_name("source"))
                })
                .and_then(|node| node.text())
                .unwrap_or(sampler);
            let image_id = effect
                .descendants()
                .find(|node| {
                    node.has_tag_name("newparam") && node.attribute("sid") == Some(surface)
                })
                .and_then(|node| {
                    node.descendants()
                        .find(|child| child.has_tag_name("surface"))
                })
                .and_then(|node| {
                    node.descendants()
                        .find(|child| child.has_tag_name("init_from"))
                })
                .and_then(|node| node.text())
                .unwrap_or(surface);
            if let Some(texture) = images.get(image_id) {
                apply_material_texture(&mut value, texture, root, load_images, codec)?;
            }
        }
        effects.insert(effect_id.to_owned(), value);
    }
    let mut result = HashMap::new();
    for material in document
        .descendants()
        .filter(|n| n.has_tag_name("material"))
    {
        let id = material.attribute("id").unwrap_or_default().to_owned();
        let effect_id = material
            .children()
            .find(|node| node.has_tag_name("instance_effect"))
            .and_then(|node| node.attribute("url"))
            .map(strip_hash);
        let mut value = effect_id
            .and_then(|effect_id| effects.get(effect_id))
            .cloned()
            .unwrap_or(ModelMaterial::new()?);
        value.id = id.clone();
        if let Some(color) = material
            .descendants()
            .find(|n| n.has_tag_name("diffuse"))
            .and_then(|n| n.descendants().find(|c| c.has_tag_name("color")))
            .and_then(|n| n.text())
        {
            apply_diffuse_color(&mut value, color)?;
        }
        if let Some(texture) = material
            .descendants()
            .find(|n| n.has_tag_name("texture"))
            .and_then(|n| n.attribute("texture"))
            .and_then(|id| images.get(id))
        {
            apply_material_texture(&mut value, texture, root, load_images, codec)?;
        }
        if let Some(name) = material.attribute("name") {
            result.insert(name.to_owned(), value.clone());
        }
        result.insert(id, value);
    }
    Ok(result)
}

fn apply_diffuse_color(material: &mut ModelMaterial, color: &str) -> Result<(), Error> {
    let values = parse_floats(color)?;
    if values.len() < 3 {
        return Err(model_error("Collada diffuse color"));
    }
    material.diffuse_color = Color4 {
        r: values[0].clamp(0.0, 1.0),
        g: values[1].clamp(0.0, 1.0),
        b: values[2].clamp(0.0, 1.0),
        a: values.get(3).copied().unwrap_or(1.0).clamp(0.0, 1.0),
    };
    Ok(())
}

fn apply_material_texture(
    material: &mut ModelMaterial,
    texture: &str,
    root: &Path,
    load_images: bool,
    codec: Option<&dyn ITextureCodec>,
) -> Result<(), Error> {
    material.texture = texture.to_owned();
    if load_images {
        material.texture_data = load_texture(root, texture, codec)?;
    }
    Ok(())
}
fn load_texture(
    root: &Path,
    relative: &str,
    codec: Option<&dyn ITextureCodec>,
) -> Result<Vec<u8>, Error> {
    if relative.len() > 4096 || relative.contains('\\') {
        return Err(model_error("Collada texture path"));
    }
    for component in Path::new(relative).components() {
        if !matches!(component, Component::Normal(_) | Component::CurDir) {
            return Err(model_error("Collada texture traversal"));
        }
    }
    let path = root
        .join(relative)
        .canonicalize()
        .map_err(|_| Error::InvalidOperation)?;
    if !path.starts_with(root) {
        return Err(model_error("Collada texture extraction root"));
    }
    let bytes = fs::read(&path).map_err(|_| Error::InvalidOperation)?;
    if bytes.len() > MAX_IMAGE_BYTES {
        return Err(Error::Argument);
    }
    let extension = path
        .extension()
        .and_then(|v| v.to_str())
        .unwrap_or_default()
        .to_ascii_lowercase();
    if extension != "jp2" && extension != "j2c" {
        let Some(codec) = codec else {
            return Err(Error::InvalidOperation);
        };
        let image = codec.decode(Box::new(std::io::Cursor::new(bytes)))?;
        #[cfg(feature = "jpeg2000")]
        {
            return libremetaverse_imaging::J2kCodec::encode(
                &image,
                libremetaverse_imaging::J2kEncodeOptions::default(),
            );
        }
        #[cfg(not(feature = "jpeg2000"))]
        {
            let _ = image;
            return Err(Error::InvalidOperation);
        }
    }
    Ok(bytes)
}

pub struct ModelUploader {
    pub include_physics_stub: bool,
    pub use_model_as_physics: bool,
    client: GridClient,
    prims: Vec<ModelPrim>,
    name: String,
    description: String,
    live_uploads: bool,
}
impl std::fmt::Debug for ModelUploader {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("ModelUploader")
            .field("prim_count", &self.prims.len())
            .field("name", &self.name)
            .field("live_uploads", &self.live_uploads)
            .finish()
    }
}
impl ModelUploader {
    pub fn new(
        client: GridClient,
        prims: Vec<ModelPrim>,
        new_inv_name: String,
        new_inv_desc: String,
    ) -> Result<Self, Error> {
        if prims.is_empty()
            || prims.len() > 10_000
            || new_inv_name.len() > 255
            || new_inv_desc.len() > 1024
        {
            return Err(Error::Argument);
        }
        Ok(Self {
            include_physics_stub: true,
            use_model_as_physics: false,
            client,
            prims,
            name: new_inv_name,
            description: new_inv_desc,
            live_uploads: false,
        })
    }
    pub fn enable_live_uploads(&mut self) {
        self.live_uploads = true;
    }
    pub fn asset_resources(&self, upload: bool) -> Result<OSD, Error> {
        let mut images = Vec::<Vec<u8>>::new();
        let mut image_index = HashMap::<String, i32>::new();
        let mut meshes = Vec::new();
        let mut instances = Vec::new();
        for prim in &self.prims {
            if prim.asset.is_empty() {
                return Err(Error::InvalidOperation);
            }
            let mut faces = Vec::new();
            for face in &prim.faces {
                let mut map = HashMap::from([
                    (
                        "diffuse_color".into(),
                        OSD::from_color4(face.material.diffuse_color)?,
                    ),
                    ("fullbright".into(), OSD::Boolean(false)),
                ]);
                if !face.material.texture_data.is_empty() {
                    let index = if let Some(index) = image_index.get(&face.material.texture) {
                        *index
                    } else {
                        let index = i32::try_from(images.len()).map_err(|_| Error::Argument)?;
                        image_index.insert(face.material.texture.clone(), index);
                        images.push(face.material.texture_data.clone());
                        index
                    };
                    map.insert("image".into(), OSD::Integer(index));
                    map.insert("scales".into(), OSD::Real(1.0));
                    map.insert("scalet".into(), OSD::Real(1.0));
                    map.insert("offsets".into(), OSD::Real(0.0));
                    map.insert("offsett".into(), OSD::Real(0.0));
                    map.insert("imagerot".into(), OSD::Real(0.0));
                }
                faces.push(OSD::Map(map));
            }
            instances.push(OSD::Map(HashMap::from([
                ("face_list".into(), OSD::Array(faces)),
                ("position".into(), OSD::from_vector3(prim.position)?),
                ("rotation".into(), OSD::from_quaternion(prim.rotation)?),
                ("scale".into(), OSD::from_vector3(prim.scale)?),
                ("material".into(), OSD::Integer(3)),
                ("physics_shape_type".into(), OSD::Integer(2)),
                (
                    "mesh".into(),
                    OSD::Integer(i32::try_from(meshes.len()).map_err(|_| Error::Argument)?),
                ),
            ])));
            meshes.push(OSD::Binary(prim.asset.clone()));
        }
        Ok(OSD::Map(HashMap::from([
            ("instance_list".into(), OSD::Array(instances)),
            ("mesh_list".into(), OSD::Array(meshes)),
            (
                "texture_list".into(),
                OSD::Array(
                    images
                        .into_iter()
                        .map(|data| OSD::Binary(if upload { data } else { Vec::new() }))
                        .collect(),
                ),
            ),
            ("metric".into(), OSD::String("MUT_Unspecified".into())),
        ])))
    }
    pub async fn prepare_upload(
        &self,
        cancellation_token: CancellationToken,
    ) -> Result<Option<OSD>, Error> {
        cancellation_token.throw_if_cancellation_requested()?;
        if !self.live_uploads {
            return Err(Error::InvalidOperation);
        }
        let network = self.client.native_network()?;
        let Some(sim) = network.native_current_sim() else {
            return Ok(None);
        };
        let Some(caps) = sim.caps else {
            return Ok(None);
        };
        let Some(uri) = caps.capability_uri("NewFileAgentInventory".into())? else {
            return Ok(None);
        };
        let inventory = self.client.native_inventory()?;
        let permission_mask =
            i32::try_from(crate::PermissionMask::ALL.0).map_err(|_| Error::InvalidOperation)?;
        let payload = OSD::Map(HashMap::from([
            ("name".into(), OSD::String(self.name.clone())),
            ("description".into(), OSD::String(self.description.clone())),
            ("asset_resources".into(), self.asset_resources(false)?),
            ("asset_type".into(), OSD::String("mesh".into())),
            ("inventory_type".into(), OSD::String("object".into())),
            (
                "folder_id".into(),
                OSD::UUID(inventory.native_find_folder_for_asset_type(AssetType::Object)),
            ),
            (
                "texture_folder_id".into(),
                OSD::UUID(inventory.native_find_folder_for_asset_type(AssetType::Texture)),
            ),
            ("everyone_mask".into(), OSD::Integer(permission_mask)),
            ("group_mask".into(), OSD::Integer(permission_mask)),
            ("next_owner_mask".into(), OSD::Integer(permission_mask)),
            (
                "expected_upload_cost".into(),
                OSD::Integer(self.client.settings_ref().upload_cost()),
            ),
        ]));
        let (_, bytes) = self
            .client
            .native_http_caps_client()
            .post_with_uri_osd_format_osd_cancellation_token_i_progress(
                uri,
                OSDFormat::Xml,
                payload,
                cancellation_token,
                None,
            )
            .await?;
        let response = OSDParser::deserialize_with_bytes(bytes)?;
        match &response {
            OSD::Map(map)
                if map
                    .get("state")
                    .and_then(|state| state.as_string().ok())
                    .as_deref()
                    == Some("upload") =>
            {
                Ok(Some(response))
            }
            _ => Ok(None),
        }
    }
    pub async fn perform_upload(
        &self,
        uploader: Uri,
        cancellation_token: CancellationToken,
    ) -> Result<Option<OSD>, Error> {
        cancellation_token.throw_if_cancellation_requested()?;
        if !self.live_uploads {
            return Err(Error::InvalidOperation);
        }
        let (_, bytes) = self
            .client
            .native_http_caps_client()
            .post_with_uri_osd_format_osd_cancellation_token_i_progress(
                uploader,
                OSDFormat::Xml,
                self.asset_resources(true)?,
                cancellation_token,
                None,
            )
            .await?;
        Ok(Some(OSDParser::deserialize_with_bytes(bytes)?))
    }
    pub async fn upload(
        &self,
        cancellation_token: CancellationToken,
    ) -> Result<Option<OSD>, Error> {
        let Some(prepared) = self.prepare_upload(cancellation_token.clone()).await? else {
            return Ok(None);
        };
        let uploader = match &prepared {
            OSD::Map(map) => map
                .get("uploader")
                .and_then(|value| value.as_uri().ok())
                .flatten(),
            _ => None,
        };
        let Some(uploader) = uploader else {
            return Ok(None);
        };
        self.perform_upload(uploader, cancellation_token).await
    }
}
