//! Native skeleton records, traversal helpers, and custom XML loading.

#![allow(clippy::missing_errors_doc)] // Public signatures are fixed by the compatibility catalog.
#![allow(clippy::must_use_candidate)] // Getters mirror C# properties.
#![allow(clippy::needless_pass_by_value)] // Owned strings are fixed by the compatibility API.
#![allow(clippy::unnecessary_wraps)] // Constructors mirror fallible mapped signatures.

use roxmltree::{Document, Node};
use std::collections::{HashMap, HashSet};
use std::sync::Arc;

const MAX_SKELETON_BYTES: u64 = 8 * 1024 * 1024;
const MAX_SKELETON_BONES: usize = 512;
const MAX_COLLISION_VOLUMES: usize = 512;
const MAX_SKELETON_DEPTH: usize = 128;

#[derive(Clone, Debug, Default, PartialEq)]
pub struct JointBase {
    name: String,
    pos: Vec<f32>,
    rot: Vec<f32>,
    scale: Vec<f32>,
    group: String,
    support: String,
    end: Vec<f32>,
    reposition: bool,
}

impl JointBase {
    pub fn new() -> Result<Self, crate::Error> {
        Ok(Self::default())
    }

    pub fn support_category(&self) -> crate::rendering::JointSupportCategory {
        if self.support.eq_ignore_ascii_case("extended") {
            crate::rendering::JointSupportCategory::Extended
        } else {
            crate::rendering::JointSupportCategory::Base
        }
    }

    pub fn end(&self) -> Vec<f32> {
        self.end.clone()
    }

    pub fn set_end(&mut self, value: Vec<f32>) {
        self.end = value;
    }

    pub fn group(&self) -> String {
        self.group.clone()
    }

    pub fn set_group(&mut self, value: String) {
        self.group = value;
    }

    pub fn name(&self) -> String {
        self.name.clone()
    }

    pub fn set_name(&mut self, value: String) {
        self.name = value;
    }

    pub fn pos(&self) -> Vec<f32> {
        self.pos.clone()
    }

    pub fn set_pos(&mut self, value: Vec<f32>) {
        self.pos = value;
    }

    pub fn reposition(&self) -> bool {
        self.reposition
    }

    pub fn set_reposition(&mut self, value: bool) {
        self.reposition = value;
    }

    pub fn rot(&self) -> Vec<f32> {
        self.rot.clone()
    }

    pub fn set_rot(&mut self, value: Vec<f32>) {
        self.rot = value;
    }

    pub fn scale(&self) -> Vec<f32> {
        self.scale.clone()
    }

    pub fn set_scale(&mut self, value: Vec<f32>) {
        self.scale = value;
    }

    pub fn support(&self) -> String {
        self.support.clone()
    }

    pub fn set_support(&mut self, value: String) {
        self.support = value;
    }

    #[allow(clippy::too_many_arguments)]
    pub(crate) fn from_parts(
        name: String,
        pos: Vec<f32>,
        rot: Vec<f32>,
        scale: Vec<f32>,
        group: String,
        support: String,
        end: Vec<f32>,
        reposition: bool,
    ) -> Self {
        Self {
            name,
            pos,
            rot,
            scale,
            group,
            support,
            end,
            reposition,
        }
    }
}

#[derive(Clone, Debug, Default, PartialEq)]
pub struct CollisionVolume {
    pub base: JointBase,
}

impl CollisionVolume {
    pub fn new() -> Result<Self, crate::Error> {
        Ok(Self::default())
    }

    pub(crate) fn from_base(base: JointBase) -> Self {
        Self { base }
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct Joint {
    pub base: JointBase,
    collision_volume: Vec<CollisionVolume>,
    bone: Option<Vec<Joint>>,
    pivot: Vec<f32>,
    aliases: String,
    connected: bool,
}

impl Default for Joint {
    fn default() -> Self {
        Self {
            base: JointBase::default(),
            collision_volume: Vec::new(),
            bone: Some(Vec::new()),
            pivot: Vec::new(),
            aliases: String::new(),
            connected: false,
        }
    }
}

impl Joint {
    pub fn new() -> Result<Self, crate::Error> {
        Ok(Self::default())
    }

    pub fn get_aliases_list(&self) -> Result<Vec<String>, crate::Error> {
        Ok(self
            .aliases
            .split_ascii_whitespace()
            .map(str::to_owned)
            .collect())
    }

    pub fn aliases(&self) -> String {
        self.aliases.clone()
    }

    pub fn set_aliases(&mut self, value: String) {
        self.aliases = value;
    }

    pub fn bone(&self) -> Option<Vec<Self>> {
        self.bone.clone()
    }

    pub fn set_bone(&mut self, value: Option<Vec<Self>>) {
        self.bone = value;
    }

    pub fn collision_volume(&self) -> Vec<CollisionVolume> {
        self.collision_volume.clone()
    }

    pub fn set_collision_volume(&mut self, value: Vec<CollisionVolume>) {
        self.collision_volume = value;
    }

    pub fn connected(&self) -> bool {
        self.connected
    }

    pub fn set_connected(&mut self, value: bool) {
        self.connected = value;
    }

    pub fn pivot(&self) -> Vec<f32> {
        self.pivot.clone()
    }

    pub fn set_pivot(&mut self, value: Vec<f32>) {
        self.pivot = value;
    }

    pub(crate) fn from_parts(
        base: JointBase,
        collision_volume: Vec<CollisionVolume>,
        bone: Vec<Self>,
        pivot: Vec<f32>,
        aliases: String,
        connected: bool,
    ) -> Self {
        Self {
            base,
            collision_volume,
            bone: Some(bone),
            pivot,
            aliases,
            connected,
        }
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct LindenSkeleton {
    bone: Joint,
    version: f32,
    version_specified: bool,
    num_bones: String,
    num_collision_volumes: String,
}

impl Default for LindenSkeleton {
    fn default() -> Self {
        Self {
            bone: Joint::default(),
            version: 0.0,
            version_specified: false,
            num_bones: String::new(),
            num_collision_volumes: String::new(),
        }
    }
}

impl LindenSkeleton {
    pub fn new() -> Result<Self, crate::Error> {
        Ok(Self::default())
    }

    pub fn build_expanded_joint_list(
        &self,
        joints_filter: Box<dyn Iterator<Item = String>>,
    ) -> Result<Vec<String>, crate::Error> {
        let filter = joints_filter.collect::<Vec<_>>();
        let mut expanded = Vec::new();
        if let Some(children) = &self.bone.bone {
            for child in children {
                expand_joint(child, &self.bone.base.name, &mut expanded, &filter);
            }
        }
        Ok(expanded)
    }

    pub fn build_joint_dictionary(&self) -> Result<HashMap<String, Arc<Joint>>, crate::Error> {
        let mut dictionary = HashMap::new();
        for joint in collect_joints(&self.bone) {
            if !joint.base.name.is_empty() {
                dictionary.insert(joint.base.name.clone(), Arc::clone(&joint));
            }
            for alias in joint.aliases.split_ascii_whitespace() {
                if !alias.is_empty() {
                    dictionary.insert(alias.to_owned(), Arc::clone(&joint));
                }
            }
        }
        Ok(dictionary)
    }

    pub fn get_all_joints(&self) -> Result<Box<dyn Iterator<Item = Arc<Joint>>>, crate::Error> {
        Ok(Box::new(collect_joints(&self.bone).into_iter()))
    }

    pub fn get_bone(&self, name_or_alias: String) -> Result<Option<Arc<Joint>>, crate::Error> {
        if name_or_alias.is_empty() {
            return Ok(None);
        }
        Ok(self.build_joint_dictionary()?.remove(&name_or_alias))
    }

    pub fn get_default() -> Result<Self, crate::Error> {
        Ok(crate::skeleton_catalog::default_skeleton())
    }

    pub fn load_with_method() -> Result<Self, crate::Error> {
        Self::get_default()
    }

    pub fn load_with_string(file_name: Option<String>) -> Result<Self, crate::Error> {
        let Some(file_name) = file_name else {
            return Self::get_default();
        };
        if std::fs::metadata(&file_name)
            .map_err(|_| crate::Error::Argument)?
            .len()
            > MAX_SKELETON_BYTES
        {
            return Err(crate::Error::Argument);
        }
        let text = std::fs::read_to_string(file_name).map_err(|_| crate::Error::Argument)?;
        if text.len() as u64 > MAX_SKELETON_BYTES {
            return Err(crate::Error::Argument);
        }
        parse_skeleton_xml(&text)
    }

    pub fn bone(&self) -> Joint {
        self.bone.clone()
    }

    pub fn set_bone(&mut self, value: Joint) {
        self.bone = value;
    }

    pub fn num_bones(&self) -> String {
        self.num_bones.clone()
    }

    pub fn set_num_bones(&mut self, value: String) {
        self.num_bones = value;
    }

    pub fn num_collision_volumes(&self) -> String {
        self.num_collision_volumes.clone()
    }

    pub fn set_num_collision_volumes(&mut self, value: String) {
        self.num_collision_volumes = value;
    }

    pub fn version(&self) -> f32 {
        self.version
    }

    pub fn set_version(&mut self, value: f32) {
        self.version = value;
    }

    pub fn version_specified(&self) -> bool {
        self.version_specified
    }

    pub fn set_version_specified(&mut self, value: bool) {
        self.version_specified = value;
    }

    pub(crate) fn from_parts(
        bone: Joint,
        version: f32,
        version_specified: bool,
        num_bones: String,
        num_collision_volumes: String,
    ) -> Self {
        Self {
            bone,
            version,
            version_specified,
            num_bones,
            num_collision_volumes,
        }
    }
}

fn collect_joints(root: &Joint) -> Vec<Arc<Joint>> {
    fn visit(joint: &Joint, output: &mut Vec<Arc<Joint>>) {
        output.push(Arc::new(joint.clone()));
        if let Some(children) = &joint.bone {
            for child in children {
                visit(child, output);
            }
        }
    }

    let mut output = Vec::new();
    visit(root, &mut output);
    output
}

fn expand_joint(
    current: &Joint,
    effective_parent: &str,
    expanded: &mut Vec<String>,
    filter: &[String],
) {
    let mut next_effective_parent = effective_parent;
    if filter.iter().any(|name| name == &current.base.name) {
        let parent_matches_last = expanded.last().is_some_and(|last| last == effective_parent);
        if !parent_matches_last && filter.iter().any(|name| name == effective_parent) {
            expanded.push(effective_parent.to_owned());
        }
        expanded.push(current.base.name.clone());
        next_effective_parent = &current.base.name;
    }
    if let Some(children) = &current.bone {
        for child in children {
            expand_joint(child, next_effective_parent, expanded, filter);
        }
    }
}

fn parse_error(position: usize) -> crate::Error {
    crate::Error::Parse {
        position,
        context: "avatar skeleton XML",
    }
}

fn node_position(node: Node<'_, '_>) -> usize {
    node.range().start
}

fn required_attribute<'a>(node: Node<'a, '_>, name: &str) -> Result<&'a str, crate::Error> {
    node.attribute(name)
        .filter(|value| !value.is_empty())
        .ok_or_else(|| parse_error(node_position(node)))
}

fn vector_attribute(node: Node<'_, '_>, name: &str) -> Result<Vec<f32>, crate::Error> {
    let Some(value) = node.attribute(name) else {
        return Ok(Vec::new());
    };
    let values = value
        .split_ascii_whitespace()
        .map(str::parse::<f32>)
        .collect::<Result<Vec<_>, _>>()
        .map_err(|_| parse_error(node_position(node)))?;
    if values.len() != 3 || values.iter().any(|value| !value.is_finite()) {
        return Err(parse_error(node_position(node)));
    }
    Ok(values)
}

fn boolean_attribute(node: Node<'_, '_>, name: &str) -> Result<bool, crate::Error> {
    match node.attribute(name) {
        None => Ok(false),
        Some(value) if value.eq_ignore_ascii_case("true") => Ok(true),
        Some(value) if value.eq_ignore_ascii_case("false") => Ok(false),
        Some(_) => Err(parse_error(node_position(node))),
    }
}

fn parse_base(node: Node<'_, '_>) -> Result<JointBase, crate::Error> {
    let support = node.attribute("support").unwrap_or_default();
    if !support.is_empty()
        && !support.eq_ignore_ascii_case("base")
        && !support.eq_ignore_ascii_case("extended")
    {
        return Err(parse_error(node_position(node)));
    }
    Ok(JointBase::from_parts(
        required_attribute(node, "name")?.to_owned(),
        vector_attribute(node, "pos")?,
        vector_attribute(node, "rot")?,
        vector_attribute(node, "scale")?,
        node.attribute("group").unwrap_or_default().to_owned(),
        support.to_owned(),
        vector_attribute(node, "end")?,
        boolean_attribute(node, "reposition")?,
    ))
}

fn parse_joint(
    node: Node<'_, '_>,
    names: &mut HashSet<String>,
    depth: usize,
    bone_count: &mut usize,
    collision_volume_count: &mut usize,
) -> Result<Joint, crate::Error> {
    if !node.has_tag_name("bone") {
        return Err(parse_error(node_position(node)));
    }
    if depth > MAX_SKELETON_DEPTH || *bone_count >= MAX_SKELETON_BONES {
        return Err(parse_error(node_position(node)));
    }
    *bone_count += 1;
    let base = parse_base(node)?;
    if !names.insert(base.name.clone()) {
        return Err(parse_error(node_position(node)));
    }
    let aliases = node.attribute("aliases").unwrap_or_default().to_owned();
    for alias in aliases.split_ascii_whitespace() {
        if !names.insert(alias.to_owned()) {
            return Err(parse_error(node_position(node)));
        }
    }
    let mut collision_volumes = Vec::new();
    let mut bones = Vec::new();
    for child in node.children().filter(Node::is_element) {
        if child.has_tag_name("collision_volume") {
            if *collision_volume_count >= MAX_COLLISION_VOLUMES {
                return Err(parse_error(node_position(child)));
            }
            if child.children().any(|node| node.is_element()) {
                return Err(parse_error(node_position(child)));
            }
            collision_volumes.push(CollisionVolume::from_base(parse_base(child)?));
            *collision_volume_count += 1;
        } else if child.has_tag_name("bone") {
            bones.push(parse_joint(
                child,
                names,
                depth + 1,
                bone_count,
                collision_volume_count,
            )?);
        } else {
            return Err(parse_error(node_position(child)));
        }
    }
    Ok(Joint::from_parts(
        base,
        collision_volumes,
        bones,
        vector_attribute(node, "pivot")?,
        aliases,
        boolean_attribute(node, "connected")?,
    ))
}

pub(crate) fn parse_skeleton_xml(text: &str) -> Result<LindenSkeleton, crate::Error> {
    if text.len() as u64 > MAX_SKELETON_BYTES {
        return Err(parse_error(0));
    }
    let document = Document::parse(text).map_err(|_| parse_error(0))?;
    let root = document.root_element();
    if !root.has_tag_name("linden_skeleton") {
        return Err(parse_error(node_position(root)));
    }
    let version_text = required_attribute(root, "version")?;
    let version = version_text
        .parse::<f32>()
        .map_err(|_| parse_error(node_position(root)))?;
    if !version.is_finite() {
        return Err(parse_error(node_position(root)));
    }
    let num_bones = required_attribute(root, "num_bones")?.to_owned();
    let expected_bones = num_bones
        .parse::<usize>()
        .map_err(|_| parse_error(node_position(root)))?;
    let num_collision_volumes = required_attribute(root, "num_collision_volumes")?.to_owned();
    let expected_collision_volumes = num_collision_volumes
        .parse::<usize>()
        .map_err(|_| parse_error(node_position(root)))?;
    if expected_bones > MAX_SKELETON_BONES || expected_collision_volumes > MAX_COLLISION_VOLUMES {
        return Err(parse_error(node_position(root)));
    }
    let root_bones = root.children().filter(Node::is_element).collect::<Vec<_>>();
    if root_bones.len() != 1 || !root_bones[0].has_tag_name("bone") {
        return Err(parse_error(node_position(root)));
    }
    let mut names = HashSet::new();
    let mut bone_count = 0;
    let mut collision_volume_count = 0;
    let bone = parse_joint(
        root_bones[0],
        &mut names,
        0,
        &mut bone_count,
        &mut collision_volume_count,
    )?;
    let (actual_bones, actual_collision_volumes) = skeleton_counts(&bone);
    if actual_bones != expected_bones || actual_collision_volumes != expected_collision_volumes {
        return Err(parse_error(node_position(root)));
    }
    Ok(LindenSkeleton::from_parts(
        bone,
        version,
        true,
        num_bones,
        num_collision_volumes,
    ))
}

pub(crate) fn skeleton_counts(root: &Joint) -> (usize, usize) {
    let mut bones = 1;
    let mut collision_volumes = root.collision_volume.len();
    if let Some(children) = &root.bone {
        for child in children {
            let (child_bones, child_collision_volumes) = skeleton_counts(child);
            bones += child_bones;
            collision_volumes += child_collision_volumes;
        }
    }
    (bones, collision_volumes)
}

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

    #[test]
    fn runtime_parser_rejects_count_mismatches_and_unknown_children() {
        let wrong_count = "<linden_skeleton version=\"1\" num_bones=\"2\" num_collision_volumes=\"0\"><bone name=\"root\"/></linden_skeleton>";
        assert!(matches!(
            parse_skeleton_xml(wrong_count),
            Err(crate::Error::Parse { .. })
        ));
        let unknown = "<linden_skeleton version=\"1\" num_bones=\"1\" num_collision_volumes=\"0\"><bone name=\"root\"><unknown/></bone></linden_skeleton>";
        assert!(matches!(
            parse_skeleton_xml(unknown),
            Err(crate::Error::Parse { .. })
        ));
    }
}
