//! Physics-driven collision-volume offsets and positional record semantics.

use crate::Error;
use libremetaverse_types::compat::{Object, Vector3};
use std::collections::HashMap;
use std::hash::{Hash, Hasher};

const PHYSICS_DRIVEN_PARAMS: [i32; 6] = [1200, 1201, 1204, 1205, 1206, 1207];

#[derive(Clone, Debug, PartialEq)]
pub struct PhysicsVolumeMorphsVolumeMorph {
    bone_name: String,
    pos_delta: Vector3,
}

impl PhysicsVolumeMorphsVolumeMorph {
    pub fn new(bone_name: String, pos_delta: Vector3) -> Result<Self, Error> {
        Ok(Self {
            bone_name,
            pos_delta,
        })
    }

    pub fn deconstruct(
        &mut self,
        bone_name: &mut String,
        pos_delta: &mut Vector3,
    ) -> Result<(), Error> {
        *bone_name = self.bone_name.clone();
        *pos_delta = self.pos_delta;
        Ok(())
    }

    #[must_use]
    pub fn equals_with_volume_morph(&self, other: Self) -> bool {
        *self == other
    }

    #[must_use]
    pub fn equals_with_object(&self, object: Object) -> bool {
        object.downcast_ref::<Self>() == Some(self)
    }

    #[must_use]
    pub fn get_hash_code(&self) -> i32 {
        let mut hasher = std::collections::hash_map::DefaultHasher::new();
        self.bone_name.hash(&mut hasher);
        for value in self.pos_delta.0 {
            value.to_bits().hash(&mut hasher);
        }
        hasher.finish() as i32
    }

    #[must_use]
    #[allow(clippy::inherent_to_string_shadow_display)]
    pub fn to_string(&self) -> String {
        format!(
            "VolumeMorph {{ BoneName = {}, PosDelta = <{}, {}, {}> }}",
            self.bone_name, self.pos_delta.0[0], self.pos_delta.0[1], self.pos_delta.0[2]
        )
    }

    #[must_use]
    pub fn eq(left: Self, right: Self) -> bool {
        left == right
    }

    #[must_use]
    pub fn ne(left: Self, right: Self) -> bool {
        left != right
    }

    #[must_use]
    pub fn bone_name(&self) -> String {
        self.bone_name.clone()
    }

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

    #[must_use]
    pub fn pos_delta(&self) -> Vector3 {
        self.pos_delta
    }

    pub fn set_pos_delta(&mut self, value: Vector3) {
        self.pos_delta = value;
    }
}

pub struct PhysicsVolumeMorphs;

impl PhysicsVolumeMorphs {
    pub fn compute_bone_offsets(
        driven_weights: HashMap<i32, f32>,
        vp_weights: HashMap<i32, f32>,
    ) -> Result<HashMap<String, Vector3>, Error> {
        // Preserved for signature parity; the pinned implementation also uses
        // only the normalized driven output and the visual-param definition.
        let _ = vp_weights;
        let mut offsets: HashMap<String, Vector3> = HashMap::new();
        for (param_id, normalized) in driven_weights {
            if !PHYSICS_DRIVEN_PARAMS.contains(&param_id) {
                continue;
            }
            let Some(parameter) = crate::visual_catalog::param_by_id(param_id) else {
                continue;
            };
            let Some(morphs) = parameter.volume_morphs.as_ref() else {
                continue;
            };
            let local_value =
                parameter.min_value + (parameter.max_value - parameter.min_value) * normalized;
            for morph in morphs {
                let delta = Vector3([
                    morph.position_delta.x * local_value,
                    morph.position_delta.y * local_value,
                    morph.position_delta.z * local_value,
                ]);
                offsets
                    .entry(morph.bone_name.clone())
                    .and_modify(|current| {
                        for index in 0..3 {
                            current.0[index] += delta.0[index];
                        }
                    })
                    .or_insert(delta);
            }
        }
        Ok(offsets)
    }
}

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

    #[test]
    fn record_properties_equality_and_deconstruction_round_trip() {
        let delta = Vector3([0.0, 0.0, -0.01]);
        let mut morph = PhysicsVolumeMorphsVolumeMorph::new("LEFT_PEC".to_owned(), delta).unwrap();
        let copy = morph.clone();
        assert!(morph.equals_with_volume_morph(copy.clone()));
        assert!(morph.equals_with_object(Object::opaque(copy)));
        let mut name = String::new();
        let mut output = Vector3::default();
        morph.deconstruct(&mut name, &mut output).unwrap();
        assert_eq!(name, "LEFT_PEC");
        assert_eq!(output, delta);
    }

    #[test]
    fn driven_weight_uses_pinned_visual_param_range_and_morphs() {
        let offsets =
            PhysicsVolumeMorphs::compute_bone_offsets(HashMap::from([(1200, 1.0)]), HashMap::new())
                .unwrap();
        assert_eq!(offsets["LEFT_PEC"], Vector3([0.0, 0.0, -0.03]));
        assert_eq!(offsets["RIGHT_PEC"], Vector3([0.0, 0.0, -0.03]));
    }
}
