use std::io::Cursor;

use binrw::BinRead;

use crate::error::{Error, Result};

use super::header::{PHYB_MAX_RECORDS_PER_SECTION, phyb_string_from_padded};

/// Byte length of the `u32` simulator count that precedes the simulator array.
pub const SIMULATOR_COUNT_PREFIX_SIZE: usize = 4;

/// Byte length of the per-simulator sub-array count block.
pub const SIMULATOR_COUNTER_BLOCK_SIZE: usize = 8;

/// Byte length of the [`SimulatorParams`] block within each simulator.
pub const SIMULATOR_PARAMS_SIZE: usize = 32;

/// Byte length of the sub-array offset block within each simulator.
pub const SIMULATOR_OFFSET_BLOCK_SIZE: usize = 32;

/// Combined byte length of the counter + params + offset blocks for one simulator.
pub const SIMULATOR_INTRINSIC_SIZE: usize =
    SIMULATOR_COUNTER_BLOCK_SIZE + SIMULATOR_PARAMS_SIZE + SIMULATOR_OFFSET_BLOCK_SIZE;

/// On-disk byte length of the fixed intrinsic fields of a [`SimChain`].
pub const SIM_CHAIN_INTRINSIC_SIZE: usize = 48;

/// On-disk byte length of a [`SimulatorCollisionRef`] record.
pub const SIM_COLLISION_REF_SIZE: usize = 36;
/// On-disk byte length of a [`SimNode`] record.
pub const SIM_NODE_SIZE: usize = 84;
/// On-disk byte length of a [`SimConnector`] record.
pub const SIM_CONNECTOR_SIZE: usize = 32;
/// On-disk byte length of a [`SimAttractor`] record.
pub const SIM_ATTRACTOR_SIZE: usize = 52;
/// On-disk byte length of a [`SimPin`] record.
pub const SIM_PIN_SIZE: usize = 48;
/// On-disk byte length of a [`SimSpring`] record.
pub const SIM_SPRING_SIZE: usize = 16;
/// On-disk byte length of a [`SimPostAlignment`] record.
pub const SIM_POST_ALIGNMENT_SIZE: usize = 36;

/// Bitfield of simulator feature flags.
///
/// Use [`SimulatorFlags::contains`] to test individual flag constants.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SimulatorFlags(u8);

impl SimulatorFlags {
    /// Simulation is active.
    pub const SIMULATING: u8 = 0x01;
    /// Collision response is enabled.
    pub const COLLISIONS_HANDLED: u8 = 0x02;
    /// Continuous collision detection is enabled.
    pub const CONTINUOUS_COLLISIONS: u8 = 0x04;
    /// Ground-plane constraint is active.
    pub const USING_GROUND_PLANE: u8 = 0x08;
    /// Chain lengths are fixed.
    pub const FIXED_LENGTH: u8 = 0x10;

    /// Constructs a [`SimulatorFlags`] from a raw byte.
    #[must_use]
    pub fn from_bits(bits: u8) -> Self {
        Self(bits)
    }

    /// Returns the raw flag byte.
    #[must_use]
    pub fn bits(self) -> u8 {
        self.0
    }

    /// Returns `true` if all bits in `bits` are set.
    #[must_use]
    pub fn contains(self, bits: u8) -> bool {
        (self.0 & bits) == bits
    }
}

/// Which side of a collision primitive a simulator responds to.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SimulatorCollisionKind {
    /// Respond on both sides.
    Both,
    /// Respond only outside the primitive.
    Outside,
    /// Respond only inside the primitive.
    Inside,
    /// Unrecognized raw value, preserved for forward compatibility.
    Unknown(i32),
}

impl SimulatorCollisionKind {
    /// Maps a raw i32 field to a [`SimulatorCollisionKind`].
    #[must_use]
    pub fn from_raw(raw: i32) -> Self {
        match raw {
            0 => SimulatorCollisionKind::Both,
            1 => SimulatorCollisionKind::Outside,
            2 => SimulatorCollisionKind::Inside,
            other => SimulatorCollisionKind::Unknown(other),
        }
    }

    /// Returns the on-disk raw value.
    #[must_use]
    pub fn as_raw(self) -> i32 {
        match self {
            SimulatorCollisionKind::Both => 0,
            SimulatorCollisionKind::Outside => 1,
            SimulatorCollisionKind::Inside => 2,
            SimulatorCollisionKind::Unknown(raw) => raw,
        }
    }
}

/// Shape type used for inter-node collision within a [`SimChain`].
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SimChainKind {
    /// Each node is a sphere.
    Sphere,
    /// Consecutive node pairs form capsules.
    Capsule,
    /// Unrecognized raw value, preserved for forward compatibility.
    Unknown(u32),
}

impl SimChainKind {
    /// Maps a raw u32 field to a [`SimChainKind`].
    #[must_use]
    pub fn from_raw(raw: u32) -> Self {
        match raw {
            0 => SimChainKind::Sphere,
            1 => SimChainKind::Capsule,
            other => SimChainKind::Unknown(other),
        }
    }

    /// Returns the on-disk raw value.
    #[must_use]
    pub fn as_raw(self) -> u32 {
        match self {
            SimChainKind::Sphere => 0,
            SimChainKind::Capsule => 1,
            SimChainKind::Unknown(raw) => raw,
        }
    }
}

/// Global physics parameters for a single simulator.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SimulatorParams {
    /// Gravity vector applied to all chains.
    pub gravity: [f32; 3],
    /// Wind vector applied to all chains.
    pub wind: [f32; 3],
    /// Number of constraint solve iterations per frame.
    pub constraint_loop: u16,
    /// Number of collision solve iterations per frame.
    pub collision_loop: u16,
    /// Feature flags for this simulator.
    pub flags: SimulatorFlags,
    /// Collision group identifier.
    pub group: u8,
}

/// A reference from a simulator to a named collision primitive.
#[derive(Debug, Clone, PartialEq)]
pub struct SimulatorCollisionRef {
    /// Name of the referenced [`CollisionData`](super::collision::CollisionData) primitive.
    pub collision_name: String,
    /// Which side of the primitive this simulator responds to.
    pub kind: SimulatorCollisionKind,
}

/// A spring chain of [`SimNode`]s driven by physics simulation.
#[derive(Debug, Clone, PartialEq)]
pub struct SimChain {
    pub dampening: f32,
    pub max_speed: f32,
    pub friction: f32,
    pub collision_dampening: f32,
    pub repulsion_strength: f32,
    /// Offset applied to the last (virtual) bone in the chain.
    pub last_bone_offset: [f32; 3],
    /// Whether nodes are spheres or capsules.
    pub chain_type: SimChainKind,
    /// Collision primitives this chain interacts with.
    pub collision_refs: Vec<SimulatorCollisionRef>,
    /// Ordered list of physics nodes along the chain.
    pub nodes: Vec<SimNode>,
}

/// A single physics node within a [`SimChain`].
#[derive(Debug, Clone, PartialEq)]
pub struct SimNode {
    /// Name of the bound skeleton bone.
    pub bone_name: String,
    pub radius: f32,
    pub attract_by_animation: f32,
    pub wind_scale: f32,
    pub gravity_scale: f32,
    pub cone_max_angle: f32,
    pub cone_axis_offset: [f32; 3],
    pub constraint_plane_normal: [f32; 3],
    pub collision_flag: u32,
    pub continuous_collision_flag: u32,
}

/// Rigid constraint linking two nodes across different chains.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SimConnector {
    pub chain_id1: u16,
    pub chain_id2: u16,
    pub node_id1: u16,
    pub node_id2: u16,
    pub collision_radius: f32,
    pub friction: f32,
    pub dampening: f32,
    pub repulsion: f32,
    pub collision_flag: u32,
    pub continuous_collision_flag: u32,
}

/// Bone-based position attractor that pulls a node toward a skeleton pose.
#[derive(Debug, Clone, PartialEq)]
pub struct SimAttractor {
    /// Name of the attracting bone.
    pub bone_name: String,
    /// Local offset from the attracting bone.
    pub bone_offset: [f32; 3],
    /// Target chain index.
    pub chain_id: u16,
    /// Target node index within the chain.
    pub node_id: u16,
    /// Attractor spring stiffness.
    pub stiffness: f32,
}

/// A pin constraint that anchors a node to a bone's position.
#[derive(Debug, Clone, PartialEq)]
pub struct SimPin {
    /// Name of the pinning bone.
    pub bone_name: String,
    /// Local offset from the pinning bone.
    pub bone_offset: [f32; 3],
    /// Target chain index.
    pub chain_id: u16,
    /// Target node index within the chain.
    pub node_id: u16,
}

/// A spring constraint between two nodes.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SimSpring {
    pub chain_id1: u16,
    pub chain_id2: u16,
    pub node_id1: u16,
    pub node_id2: u16,
    pub stretch_stiffness: f32,
    pub compress_stiffness: f32,
}

/// Post-simulation alignment constraint tied to a named collision primitive.
#[derive(Debug, Clone, PartialEq)]
pub struct SimPostAlignment {
    /// Name of the referenced collision primitive.
    pub collision_name: String,
    pub chain_id: u16,
    pub node_id: u16,
}

/// All data for a single cloth/physics simulator instance.
#[derive(Debug, Clone, PartialEq)]
pub struct Simulator {
    /// Global physics parameters for this simulator.
    pub params: SimulatorParams,
    /// Collision objects this simulator checks against.
    pub collision_objects: Vec<SimulatorCollisionRef>,
    /// Collision connectors for inter-chain interaction.
    pub collision_connectors: Vec<SimulatorCollisionRef>,
    /// Spring chains with their nodes.
    pub chains: Vec<SimChain>,
    /// Rigid constraints between nodes.
    pub connectors: Vec<SimConnector>,
    /// Bone attractors.
    pub attractors: Vec<SimAttractor>,
    /// Pin constraints.
    pub pins: Vec<SimPin>,
    /// Spring constraints.
    pub springs: Vec<SimSpring>,
    /// Post-alignment constraints.
    pub post_alignments: Vec<SimPostAlignment>,
}

/// All simulator instances from a single `.phyb` simulator section.
#[derive(Debug, Clone, Default, PartialEq)]
pub struct SimulatorData {
    pub simulators: Vec<Simulator>,
}

impl SimulatorData {
    pub(crate) fn parse(bytes: &[u8], start: usize) -> Result<Self> {
        let file_size = u64::try_from(bytes.len()).unwrap_or(u64::MAX);

        let prefix_end =
            start
                .checked_add(SIMULATOR_COUNT_PREFIX_SIZE)
                .ok_or(Error::InvalidPhybOffset {
                    field: "simulator.count_prefix",
                    offset: u64::MAX,
                    file_size,
                })?;
        if prefix_end > bytes.len() {
            return Err(Error::InvalidPhybLength {
                found: bytes.len(),
                minimum: prefix_end,
            });
        }
        let mut cursor = Cursor::new(&bytes[start..prefix_end]);
        let num_simulators =
            u32::read_le(&mut cursor).map_err(|e| Error::decode("phyb.simulator.count", &e))?;
        if num_simulators > PHYB_MAX_RECORDS_PER_SECTION {
            return Err(Error::InvalidPhybCount {
                kind: "num_simulators",
                count: num_simulators,
                max: PHYB_MAX_RECORDS_PER_SECTION,
            });
        }

        let mut cursor_pos = prefix_end;
        let mut simulators = Vec::with_capacity(
            usize::try_from(num_simulators)
                .expect("num_simulators is bounded by PHYB_MAX_RECORDS_PER_SECTION"),
        );
        for index in 0..num_simulators {
            let sim = parse_one_simulator(bytes, start, &mut cursor_pos, index)?;
            simulators.push(sim);
        }

        Ok(Self { simulators })
    }
}

struct ParsedSimulatorIntrinsic {
    counters: WireSimulatorCounters,
    params: WireSimulatorParams,
    offsets: WireSimulatorOffsets,
    end: usize,
}

fn read_simulator_intrinsic(bytes: &[u8], cursor_pos: usize) -> Result<ParsedSimulatorIntrinsic> {
    let end = cursor_pos
        .checked_add(SIMULATOR_INTRINSIC_SIZE)
        .ok_or(Error::InvalidPhybOffset {
            field: "simulator.intrinsic",
            offset: u64::MAX,
            file_size: u64::try_from(bytes.len()).unwrap_or(u64::MAX),
        })?;
    if end > bytes.len() {
        return Err(Error::InvalidPhybLength {
            found: bytes.len(),
            minimum: end,
        });
    }
    let mut cur = Cursor::new(&bytes[cursor_pos..end]);
    let counters = WireSimulatorCounters::read(&mut cur)
        .map_err(|e| Error::decode("phyb.simulator.counters", &e))?;

    for (kind, raw) in [
        ("num_collision_data", u32::from(counters.collision_data)),
        (
            "num_collision_connector",
            u32::from(counters.collision_connector),
        ),
        ("num_chain", u32::from(counters.chain)),
        ("num_connector", u32::from(counters.connector)),
        ("num_attract", u32::from(counters.attract)),
        ("num_pin", u32::from(counters.pin)),
        ("num_spring", u32::from(counters.spring)),
        ("num_post_alignment", u32::from(counters.post_alignment)),
    ] {
        if raw > PHYB_MAX_RECORDS_PER_SECTION {
            return Err(Error::InvalidPhybCount {
                kind,
                count: raw,
                max: PHYB_MAX_RECORDS_PER_SECTION,
            });
        }
    }

    let params = WireSimulatorParams::read(&mut cur)
        .map_err(|e| Error::decode("phyb.simulator.params", &e))?;
    let offsets = WireSimulatorOffsets::read(&mut cur)
        .map_err(|e| Error::decode("phyb.simulator.offsets", &e))?;
    Ok(ParsedSimulatorIntrinsic {
        counters,
        params,
        offsets,
        end,
    })
}

#[allow(clippy::too_many_arguments)]
fn read_named_into<T>(
    bytes: &[u8],
    simulator_start: usize,
    sub_offset: u32,
    count: usize,
    record_size: usize,
    context: &'static str,
    decode_one: fn(&mut Cursor<&[u8]>, u64) -> Result<T>,
    windows: &mut Vec<SubArrayWindow>,
) -> Result<Vec<T>> {
    let (out, window) = read_named_array(
        bytes,
        simulator_start,
        sub_offset,
        count,
        record_size,
        context,
        decode_one,
    )?;
    if let Some(w) = window {
        windows.push(w);
    }
    Ok(out)
}

fn read_simulator_sub_arrays(
    bytes: &[u8],
    simulator_start: usize,
    intrinsic: &ParsedSimulatorIntrinsic,
    simulator_index: u32,
) -> Result<SimulatorSubArrays> {
    let counters = &intrinsic.counters;
    let offsets = &intrinsic.offsets;
    let mut windows: Vec<SubArrayWindow> = Vec::new();

    let collision_objects = read_named_into(
        bytes,
        simulator_start,
        offsets.collision_object,
        usize::from(counters.collision_data),
        SIM_COLLISION_REF_SIZE,
        "phyb.simulator.collision_object",
        parse_collision_ref,
        &mut windows,
    )?;
    let collision_connectors = read_named_into(
        bytes,
        simulator_start,
        offsets.collision_connector,
        usize::from(counters.collision_connector),
        SIM_COLLISION_REF_SIZE,
        "phyb.simulator.collision_connector",
        parse_collision_ref,
        &mut windows,
    )?;
    let (chains, chain_windows) = read_chains(
        bytes,
        simulator_start,
        offsets.chain,
        usize::from(counters.chain),
    )?;
    windows.extend(chain_windows);
    let connectors = read_named_into(
        bytes,
        simulator_start,
        offsets.connector,
        usize::from(counters.connector),
        SIM_CONNECTOR_SIZE,
        "phyb.simulator.connector",
        parse_connector,
        &mut windows,
    )?;
    let attractors = read_named_into(
        bytes,
        simulator_start,
        offsets.attract,
        usize::from(counters.attract),
        SIM_ATTRACTOR_SIZE,
        "phyb.simulator.attractor",
        parse_attractor,
        &mut windows,
    )?;
    let pins = read_named_into(
        bytes,
        simulator_start,
        offsets.pin,
        usize::from(counters.pin),
        SIM_PIN_SIZE,
        "phyb.simulator.pin",
        parse_pin,
        &mut windows,
    )?;
    let springs = read_named_into(
        bytes,
        simulator_start,
        offsets.spring,
        usize::from(counters.spring),
        SIM_SPRING_SIZE,
        "phyb.simulator.spring",
        parse_spring,
        &mut windows,
    )?;
    let post_alignments = read_named_into(
        bytes,
        simulator_start,
        offsets.post_alignment,
        usize::from(counters.post_alignment),
        SIM_POST_ALIGNMENT_SIZE,
        "phyb.simulator.post_alignment",
        parse_post_alignment,
        &mut windows,
    )?;

    check_sub_array_overlap(simulator_index, &mut windows)?;

    Ok(SimulatorSubArrays {
        collision_objects,
        collision_connectors,
        chains,
        connectors,
        attractors,
        pins,
        springs,
        post_alignments,
    })
}

fn check_sub_array_overlap(simulator_index: u32, windows: &mut [SubArrayWindow]) -> Result<()> {
    windows.sort_by_key(|w| w.start);
    for pair in windows.windows(2) {
        let prev = pair[0];
        let next = pair[1];
        if next.start < prev.end {
            return Err(Error::InvalidPhybSubArrayOverlap {
                simulator_index,
                prev_field: prev.field,
                prev_end: prev.end,
                next_field: next.field,
                next_start: next.start,
            });
        }
    }
    Ok(())
}

struct SimulatorSubArrays {
    collision_objects: Vec<SimulatorCollisionRef>,
    collision_connectors: Vec<SimulatorCollisionRef>,
    chains: Vec<SimChain>,
    connectors: Vec<SimConnector>,
    attractors: Vec<SimAttractor>,
    pins: Vec<SimPin>,
    springs: Vec<SimSpring>,
    post_alignments: Vec<SimPostAlignment>,
}

fn parse_one_simulator(
    bytes: &[u8],
    simulator_start: usize,
    cursor_pos: &mut usize,
    simulator_index: u32,
) -> Result<Simulator> {
    let intrinsic = read_simulator_intrinsic(bytes, *cursor_pos)?;
    let sub = read_simulator_sub_arrays(bytes, simulator_start, &intrinsic, simulator_index)?;
    *cursor_pos = intrinsic.end;
    Ok(Simulator {
        params: SimulatorParams {
            gravity: intrinsic.params.gravity,
            wind: intrinsic.params.wind,
            constraint_loop: intrinsic.params.constraint_loop,
            collision_loop: intrinsic.params.collision_loop,
            flags: SimulatorFlags::from_bits(intrinsic.params.flags),
            group: intrinsic.params.group,
        },
        collision_objects: sub.collision_objects,
        collision_connectors: sub.collision_connectors,
        chains: sub.chains,
        connectors: sub.connectors,
        attractors: sub.attractors,
        pins: sub.pins,
        springs: sub.springs,
        post_alignments: sub.post_alignments,
    })
}

fn resolve_sub_offset(
    simulator_start: usize,
    sub_offset: u32,
    file_size: u64,
    field: &'static str,
) -> Result<usize> {
    let sub = u64::from(sub_offset);
    let prefix = u64::try_from(SIMULATOR_COUNT_PREFIX_SIZE).unwrap_or(u64::MAX);
    let start = u64::try_from(simulator_start).unwrap_or(u64::MAX);
    let resolved = start
        .checked_add(prefix)
        .and_then(|s| s.checked_add(sub))
        .ok_or(Error::InvalidPhybOffset {
            field,
            offset: u64::MAX,
            file_size,
        })?;
    if resolved > file_size {
        return Err(Error::InvalidPhybOffset {
            field,
            offset: resolved,
            file_size,
        });
    }
    usize::try_from(resolved).map_err(|_| Error::InvalidPhybOffset {
        field,
        offset: resolved,
        file_size,
    })
}

fn read_named_array<T>(
    bytes: &[u8],
    simulator_start: usize,
    sub_offset: u32,
    count: usize,
    record_size: usize,
    context: &'static str,
    decode_one: fn(&mut Cursor<&[u8]>, u64) -> Result<T>,
) -> Result<(Vec<T>, Option<SubArrayWindow>)> {
    if count == 0 {
        return Ok((Vec::new(), None));
    }
    let file_size = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
    let start_pos = resolve_sub_offset(simulator_start, sub_offset, file_size, context)?;
    let total = record_size
        .checked_mul(count)
        .ok_or(Error::InvalidPhybOffset {
            field: context,
            offset: u64::MAX,
            file_size,
        })?;
    let end = start_pos
        .checked_add(total)
        .ok_or(Error::InvalidPhybOffset {
            field: context,
            offset: u64::MAX,
            file_size,
        })?;
    if end > bytes.len() {
        return Err(Error::InvalidPhybLength {
            found: bytes.len(),
            minimum: end,
        });
    }
    let mut cur = Cursor::new(&bytes[start_pos..end]);
    let mut out = Vec::with_capacity(count);
    for i in 0..count {
        let record_offset = u64::try_from(start_pos + i * record_size).unwrap_or(u64::MAX);
        out.push(decode_one(&mut cur, record_offset)?);
    }
    let window = SubArrayWindow {
        field: context,
        start: u64::try_from(start_pos).unwrap_or(u64::MAX),
        end: u64::try_from(end).unwrap_or(u64::MAX),
    };
    Ok((out, Some(window)))
}

#[derive(Debug, Clone, Copy)]
struct SubArrayWindow {
    field: &'static str,
    start: u64,
    end: u64,
}

fn read_chains(
    bytes: &[u8],
    simulator_start: usize,
    chain_offset: u32,
    count: usize,
) -> Result<(Vec<SimChain>, Vec<SubArrayWindow>)> {
    if count == 0 {
        return Ok((Vec::new(), Vec::new()));
    }
    let file_size = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
    let start_pos =
        resolve_sub_offset(simulator_start, chain_offset, file_size, "phyb.chain.array")?;
    let total = SIM_CHAIN_INTRINSIC_SIZE
        .checked_mul(count)
        .ok_or(Error::InvalidPhybOffset {
            field: "phyb.chain.array",
            offset: u64::MAX,
            file_size,
        })?;
    let end = start_pos
        .checked_add(total)
        .ok_or(Error::InvalidPhybOffset {
            field: "phyb.chain.array",
            offset: u64::MAX,
            file_size,
        })?;
    if end > bytes.len() {
        return Err(Error::InvalidPhybLength {
            found: bytes.len(),
            minimum: end,
        });
    }
    let mut cur = Cursor::new(&bytes[start_pos..end]);
    let mut out = Vec::with_capacity(count);
    let mut windows = Vec::with_capacity(count * 2 + 1);
    windows.push(SubArrayWindow {
        field: "phyb.chain.array",
        start: u64::try_from(start_pos).unwrap_or(u64::MAX),
        end: u64::try_from(end).unwrap_or(u64::MAX),
    });
    for _ in 0..count {
        let wire = WireSimChain::read(&mut cur).map_err(|e| Error::decode("phyb.chain", &e))?;
        if u32::from(wire.num_collisions) > PHYB_MAX_RECORDS_PER_SECTION {
            return Err(Error::InvalidPhybCount {
                kind: "chain.num_collisions",
                count: u32::from(wire.num_collisions),
                max: PHYB_MAX_RECORDS_PER_SECTION,
            });
        }
        if u32::from(wire.num_nodes) > PHYB_MAX_RECORDS_PER_SECTION {
            return Err(Error::InvalidPhybCount {
                kind: "chain.num_nodes",
                count: u32::from(wire.num_nodes),
                max: PHYB_MAX_RECORDS_PER_SECTION,
            });
        }

        let (collision_refs, cref_window) = read_named_array(
            bytes,
            simulator_start,
            wire.chain_collision_offset,
            usize::from(wire.num_collisions),
            SIM_COLLISION_REF_SIZE,
            "phyb.chain.collision_ref",
            parse_collision_ref,
        )?;
        if let Some(w) = cref_window {
            windows.push(w);
        }
        let (nodes, node_window) = read_named_array(
            bytes,
            simulator_start,
            wire.chain_node_offset,
            usize::from(wire.num_nodes),
            SIM_NODE_SIZE,
            "phyb.chain.node",
            parse_node,
        )?;
        if let Some(w) = node_window {
            windows.push(w);
        }

        out.push(SimChain {
            dampening: wire.dampening,
            max_speed: wire.max_speed,
            friction: wire.friction,
            collision_dampening: wire.collision_dampening,
            repulsion_strength: wire.repulsion_strength,
            last_bone_offset: wire.last_bone_offset,
            chain_type: SimChainKind::from_raw(wire.chain_type),
            collision_refs,
            nodes,
        });
    }
    Ok((out, windows))
}

fn parse_collision_ref(
    cur: &mut Cursor<&[u8]>,
    record_offset: u64,
) -> Result<SimulatorCollisionRef> {
    let wire = WireCollisionRef::read(cur).map_err(|e| Error::decode("phyb.collision_ref", &e))?;
    Ok(SimulatorCollisionRef {
        collision_name: phyb_string_from_padded(
            &wire.collision_name,
            "collision_ref.collision_name",
            record_offset,
        )?,
        kind: SimulatorCollisionKind::from_raw(wire.kind),
    })
}

fn parse_node(cur: &mut Cursor<&[u8]>, record_offset: u64) -> Result<SimNode> {
    let wire = WireSimNode::read(cur).map_err(|e| Error::decode("phyb.node", &e))?;
    Ok(SimNode {
        bone_name: phyb_string_from_padded(&wire.bone_name, "node.bone_name", record_offset)?,
        radius: wire.radius,
        attract_by_animation: wire.attract_by_animation,
        wind_scale: wire.wind_scale,
        gravity_scale: wire.gravity_scale,
        cone_max_angle: wire.cone_max_angle,
        cone_axis_offset: wire.cone_axis_offset,
        constraint_plane_normal: wire.constraint_plane_normal,
        collision_flag: wire.collision_flag,
        continuous_collision_flag: wire.continuous_collision_flag,
    })
}

fn parse_connector(cur: &mut Cursor<&[u8]>, _record_offset: u64) -> Result<SimConnector> {
    let wire = WireSimConnector::read(cur).map_err(|e| Error::decode("phyb.connector", &e))?;
    Ok(SimConnector {
        chain_id1: wire.chain_id1,
        chain_id2: wire.chain_id2,
        node_id1: wire.node_id1,
        node_id2: wire.node_id2,
        collision_radius: wire.collision_radius,
        friction: wire.friction,
        dampening: wire.dampening,
        repulsion: wire.repulsion,
        collision_flag: wire.collision_flag,
        continuous_collision_flag: wire.continuous_collision_flag,
    })
}

fn parse_attractor(cur: &mut Cursor<&[u8]>, record_offset: u64) -> Result<SimAttractor> {
    let wire = WireSimAttractor::read(cur).map_err(|e| Error::decode("phyb.attractor", &e))?;
    Ok(SimAttractor {
        bone_name: phyb_string_from_padded(&wire.bone_name, "attractor.bone_name", record_offset)?,
        bone_offset: wire.bone_offset,
        chain_id: wire.chain_id,
        node_id: wire.node_id,
        stiffness: wire.stiffness,
    })
}

fn parse_pin(cur: &mut Cursor<&[u8]>, record_offset: u64) -> Result<SimPin> {
    let wire = WireSimPin::read(cur).map_err(|e| Error::decode("phyb.pin", &e))?;
    Ok(SimPin {
        bone_name: phyb_string_from_padded(&wire.bone_name, "pin.bone_name", record_offset)?,
        bone_offset: wire.bone_offset,
        chain_id: wire.chain_id,
        node_id: wire.node_id,
    })
}

fn parse_spring(cur: &mut Cursor<&[u8]>, _record_offset: u64) -> Result<SimSpring> {
    let wire = WireSimSpring::read(cur).map_err(|e| Error::decode("phyb.spring", &e))?;
    Ok(SimSpring {
        chain_id1: wire.chain_id1,
        chain_id2: wire.chain_id2,
        node_id1: wire.node_id1,
        node_id2: wire.node_id2,
        stretch_stiffness: wire.stretch_stiffness,
        compress_stiffness: wire.compress_stiffness,
    })
}

fn parse_post_alignment(cur: &mut Cursor<&[u8]>, record_offset: u64) -> Result<SimPostAlignment> {
    let wire =
        WireSimPostAlignment::read(cur).map_err(|e| Error::decode("phyb.post_alignment", &e))?;
    Ok(SimPostAlignment {
        collision_name: phyb_string_from_padded(
            &wire.collision_name,
            "post_alignment.collision_name",
            record_offset,
        )?,
        chain_id: wire.chain_id,
        node_id: wire.node_id,
    })
}

#[derive(Debug, BinRead)]
#[brw(little)]
struct WireSimulatorCounters {
    collision_data: u8,
    collision_connector: u8,
    chain: u8,
    connector: u8,
    attract: u8,
    pin: u8,
    spring: u8,
    post_alignment: u8,
}

#[derive(Debug, BinRead)]
#[brw(little)]
struct WireSimulatorParams {
    gravity: [f32; 3],
    wind: [f32; 3],
    constraint_loop: u16,
    collision_loop: u16,
    flags: u8,
    group: u8,
    #[allow(dead_code)]
    padding: [u8; 2],
}

#[derive(Debug, BinRead)]
#[brw(little)]
struct WireSimulatorOffsets {
    collision_object: u32,
    collision_connector: u32,
    chain: u32,
    connector: u32,
    attract: u32,
    pin: u32,
    spring: u32,
    post_alignment: u32,
}

#[derive(Debug, BinRead)]
#[brw(little)]
struct WireSimChain {
    num_collisions: u16,
    num_nodes: u16,
    dampening: f32,
    max_speed: f32,
    friction: f32,
    collision_dampening: f32,
    repulsion_strength: f32,
    last_bone_offset: [f32; 3],
    chain_type: u32,
    chain_collision_offset: u32,
    chain_node_offset: u32,
}

#[derive(Debug, BinRead)]
#[brw(little)]
struct WireCollisionRef {
    collision_name: [u8; 32],
    kind: i32,
}

#[derive(Debug, BinRead)]
#[brw(little)]
struct WireSimNode {
    bone_name: [u8; 32],
    radius: f32,
    attract_by_animation: f32,
    wind_scale: f32,
    gravity_scale: f32,
    cone_max_angle: f32,
    cone_axis_offset: [f32; 3],
    constraint_plane_normal: [f32; 3],
    collision_flag: u32,
    continuous_collision_flag: u32,
}

#[derive(Debug, BinRead)]
#[brw(little)]
struct WireSimConnector {
    chain_id1: u16,
    chain_id2: u16,
    node_id1: u16,
    node_id2: u16,
    collision_radius: f32,
    friction: f32,
    dampening: f32,
    repulsion: f32,
    collision_flag: u32,
    continuous_collision_flag: u32,
}

#[derive(Debug, BinRead)]
#[brw(little)]
struct WireSimAttractor {
    bone_name: [u8; 32],
    bone_offset: [f32; 3],
    chain_id: u16,
    node_id: u16,
    stiffness: f32,
}

#[derive(Debug, BinRead)]
#[brw(little)]
struct WireSimPin {
    bone_name: [u8; 32],
    bone_offset: [f32; 3],
    chain_id: u16,
    node_id: u16,
}

#[derive(Debug, BinRead)]
#[brw(little)]
struct WireSimSpring {
    chain_id1: u16,
    chain_id2: u16,
    node_id1: u16,
    node_id2: u16,
    stretch_stiffness: f32,
    compress_stiffness: f32,
}

#[derive(Debug, BinRead)]
#[brw(little)]
struct WireSimPostAlignment {
    collision_name: [u8; 32],
    chain_id: u16,
    node_id: u16,
}

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

    fn name_buf(s: &str) -> [u8; 32] {
        let mut out = [0u8; 32];
        let bytes = s.as_bytes();
        let n = bytes.len().min(32);
        out[..n].copy_from_slice(&bytes[..n]);
        out
    }

    #[test]
    fn simulator_flags_round_trip() {
        let f = SimulatorFlags::from_bits(0x07);
        assert_eq!(f.bits(), 0x07);
        assert!(f.contains(SimulatorFlags::SIMULATING));
        assert!(f.contains(SimulatorFlags::COLLISIONS_HANDLED));
        assert!(f.contains(SimulatorFlags::CONTINUOUS_COLLISIONS));
        assert!(!f.contains(SimulatorFlags::USING_GROUND_PLANE));
    }

    #[test]
    fn collision_kind_from_raw_known_values() {
        assert_eq!(
            SimulatorCollisionKind::from_raw(0),
            SimulatorCollisionKind::Both
        );
        assert_eq!(
            SimulatorCollisionKind::from_raw(1),
            SimulatorCollisionKind::Outside
        );
        assert_eq!(
            SimulatorCollisionKind::from_raw(2),
            SimulatorCollisionKind::Inside
        );
    }

    #[test]
    fn collision_kind_unknown_round_trip() {
        let k = SimulatorCollisionKind::from_raw(42);
        assert_eq!(k, SimulatorCollisionKind::Unknown(42));
        assert_eq!(k.as_raw(), 42);
    }

    #[test]
    fn chain_kind_unknown_round_trip() {
        let k = SimChainKind::from_raw(7);
        assert_eq!(k, SimChainKind::Unknown(7));
        assert_eq!(k.as_raw(), 7);
        assert_eq!(SimChainKind::from_raw(0), SimChainKind::Sphere);
        assert_eq!(SimChainKind::from_raw(1), SimChainKind::Capsule);
    }

    fn build_empty_simulator_section() -> Vec<u8> {
        let mut buf = Vec::new();
        buf.extend_from_slice(&1u32.to_le_bytes());
        buf.extend_from_slice(&[0u8; SIMULATOR_COUNTER_BLOCK_SIZE]);
        for v in [0.0f32, -9.8, 0.0, 1.0, 0.0, 0.0] {
            buf.extend_from_slice(&v.to_le_bytes());
        }
        buf.extend_from_slice(&4u16.to_le_bytes());
        buf.extend_from_slice(&2u16.to_le_bytes());
        buf.push(0x03);
        buf.push(0x05);
        buf.extend_from_slice(&[0u8; 2]);
        for _ in 0..8 {
            buf.extend_from_slice(&0u32.to_le_bytes());
        }
        assert_eq!(
            buf.len(),
            SIMULATOR_COUNT_PREFIX_SIZE + SIMULATOR_INTRINSIC_SIZE
        );
        buf
    }

    #[test]
    fn parses_empty_single_simulator() {
        let bytes = build_empty_simulator_section();
        let sim_data = SimulatorData::parse(&bytes, 0).expect("parse");
        assert_eq!(sim_data.simulators.len(), 1);
        let sim = &sim_data.simulators[0];
        assert!(sim.chains.is_empty());
        assert!(sim.collision_objects.is_empty());
        assert!(sim.connectors.is_empty());
        assert!(sim.attractors.is_empty());
        assert!(sim.pins.is_empty());
        assert!(sim.springs.is_empty());
        assert!(sim.post_alignments.is_empty());
        assert!((sim.params.gravity[1] + 9.8).abs() < f32::EPSILON);
        assert_eq!(sim.params.flags.bits(), 0x03);
        assert_eq!(sim.params.group, 0x05);
        assert_eq!(sim.params.constraint_loop, 4);
        assert_eq!(sim.params.collision_loop, 2);
    }

    #[test]
    fn parses_simulator_with_one_pin() {
        let mut buf = Vec::new();
        buf.extend_from_slice(&1u32.to_le_bytes());
        let mut counters = [0u8; SIMULATOR_COUNTER_BLOCK_SIZE];
        counters[5] = 1;
        buf.extend_from_slice(&counters);
        buf.extend_from_slice(&[0u8; SIMULATOR_PARAMS_SIZE]);
        let mut offsets = [0u32; 8];
        offsets[5] = u32::try_from(SIMULATOR_INTRINSIC_SIZE).expect("intrinsic size fits u32");
        for o in offsets {
            buf.extend_from_slice(&o.to_le_bytes());
        }
        assert_eq!(
            buf.len(),
            SIMULATOR_COUNT_PREFIX_SIZE + SIMULATOR_INTRINSIC_SIZE
        );

        let mut pin = Vec::with_capacity(SIM_PIN_SIZE);
        pin.extend_from_slice(&name_buf("j_hair_a"));
        pin.extend_from_slice(&0.1f32.to_le_bytes());
        pin.extend_from_slice(&0.2f32.to_le_bytes());
        pin.extend_from_slice(&0.3f32.to_le_bytes());
        pin.extend_from_slice(&3u16.to_le_bytes());
        pin.extend_from_slice(&7u16.to_le_bytes());
        assert_eq!(pin.len(), SIM_PIN_SIZE);
        buf.extend_from_slice(&pin);

        let sim_data = SimulatorData::parse(&buf, 0).expect("parse");
        let sim = &sim_data.simulators[0];
        assert_eq!(sim.pins.len(), 1);
        let p = &sim.pins[0];
        assert_eq!(p.bone_name, "j_hair_a");
        assert_eq!(p.chain_id, 3);
        assert_eq!(p.node_id, 7);
    }

    #[test]
    fn rejects_truncated_prefix() {
        let bytes = vec![0u8; 2];
        let err = SimulatorData::parse(&bytes, 0).unwrap_err();
        assert!(matches!(err, Error::InvalidPhybLength { .. }));
    }

    #[test]
    fn rejects_too_many_simulators() {
        let mut buf = Vec::new();
        buf.extend_from_slice(&(PHYB_MAX_RECORDS_PER_SECTION + 1).to_le_bytes());
        let err = SimulatorData::parse(&buf, 0).unwrap_err();
        match err {
            Error::InvalidPhybCount { kind, count, max } => {
                assert_eq!(kind, "num_simulators");
                assert_eq!(count, PHYB_MAX_RECORDS_PER_SECTION + 1);
                assert_eq!(max, PHYB_MAX_RECORDS_PER_SECTION);
            }
            other => panic!("expected InvalidPhybCount, got {other:?}"),
        }
    }

    #[test]
    fn rejects_simulator_with_unreachable_pin_offset() {
        let mut buf = Vec::new();
        buf.extend_from_slice(&1u32.to_le_bytes());
        let mut counters = [0u8; SIMULATOR_COUNTER_BLOCK_SIZE];
        counters[5] = 1;
        buf.extend_from_slice(&counters);
        buf.extend_from_slice(&[0u8; SIMULATOR_PARAMS_SIZE]);
        let mut offsets = [0u32; 8];
        offsets[5] = 999_999;
        for o in offsets {
            buf.extend_from_slice(&o.to_le_bytes());
        }

        let err = SimulatorData::parse(&buf, 0).unwrap_err();
        assert!(matches!(err, Error::InvalidPhybOffset { .. }));
    }

    #[test]
    fn parses_chain_with_node_via_sub_offset() {
        let mut buf = Vec::new();
        buf.extend_from_slice(&1u32.to_le_bytes());
        let mut counters = [0u8; SIMULATOR_COUNTER_BLOCK_SIZE];
        counters[2] = 1;
        buf.extend_from_slice(&counters);
        buf.extend_from_slice(&[0u8; SIMULATOR_PARAMS_SIZE]);
        let mut offsets = [0u32; 8];
        offsets[2] = 72;
        for o in offsets {
            buf.extend_from_slice(&o.to_le_bytes());
        }
        assert_eq!(
            buf.len(),
            SIMULATOR_COUNT_PREFIX_SIZE + SIMULATOR_INTRINSIC_SIZE
        );

        let mut chain = Vec::with_capacity(SIM_CHAIN_INTRINSIC_SIZE);
        chain.extend_from_slice(&0u16.to_le_bytes());
        chain.extend_from_slice(&1u16.to_le_bytes());
        for v in [0.1f32, 1.0, 0.5, 0.2, 0.3] {
            chain.extend_from_slice(&v.to_le_bytes());
        }
        chain.extend_from_slice(&0.0f32.to_le_bytes());
        chain.extend_from_slice(&0.0f32.to_le_bytes());
        chain.extend_from_slice(&0.0f32.to_le_bytes());
        chain.extend_from_slice(&1u32.to_le_bytes());
        chain.extend_from_slice(&0u32.to_le_bytes());
        chain.extend_from_slice(&120u32.to_le_bytes());
        assert_eq!(chain.len(), SIM_CHAIN_INTRINSIC_SIZE);
        buf.extend_from_slice(&chain);

        let mut node = Vec::with_capacity(SIM_NODE_SIZE);
        node.extend_from_slice(&name_buf("j_skirt_a"));
        node.extend_from_slice(&1.5f32.to_le_bytes());
        node.extend_from_slice(&0.2f32.to_le_bytes());
        node.extend_from_slice(&1.0f32.to_le_bytes());
        node.extend_from_slice(&1.0f32.to_le_bytes());
        node.extend_from_slice(&0.5f32.to_le_bytes());
        for _ in 0..6 {
            node.extend_from_slice(&0.0f32.to_le_bytes());
        }
        node.extend_from_slice(&0x0F0F_F0F0u32.to_le_bytes());
        node.extend_from_slice(&0x0000_0001u32.to_le_bytes());
        assert_eq!(node.len(), SIM_NODE_SIZE);
        buf.extend_from_slice(&node);

        let sim_data = SimulatorData::parse(&buf, 0).expect("parse");
        let sim = &sim_data.simulators[0];
        assert_eq!(sim.chains.len(), 1);
        let ch = &sim.chains[0];
        assert_eq!(ch.chain_type, SimChainKind::Capsule);
        assert_eq!(ch.collision_refs.len(), 0);
        assert_eq!(ch.nodes.len(), 1);
        let n = &ch.nodes[0];
        assert_eq!(n.bone_name, "j_skirt_a");
        assert!((n.radius - 1.5).abs() < f32::EPSILON);
        assert_eq!(n.collision_flag, 0x0F0F_F0F0);
        assert_eq!(n.continuous_collision_flag, 0x0000_0001);
    }

    #[test]
    fn rejects_simulator_sub_array_overlap() {
        let mut buf = Vec::new();
        buf.extend_from_slice(&1u32.to_le_bytes());
        let mut counters = [0u8; SIMULATOR_COUNTER_BLOCK_SIZE];
        counters[4] = 1;
        counters[5] = 1;
        buf.extend_from_slice(&counters);
        buf.extend_from_slice(&[0u8; SIMULATOR_PARAMS_SIZE]);
        let mut offsets = [0u32; 8];
        offsets[4] = u32::try_from(SIMULATOR_INTRINSIC_SIZE).expect("fits");
        offsets[5] = u32::try_from(SIMULATOR_INTRINSIC_SIZE + 24).expect("fits");
        for o in offsets {
            buf.extend_from_slice(&o.to_le_bytes());
        }
        buf.resize(200, 0);

        let err = SimulatorData::parse(&buf, 0).unwrap_err();
        match err {
            Error::InvalidPhybSubArrayOverlap {
                simulator_index, ..
            } => {
                assert_eq!(simulator_index, 0);
            }
            other => panic!("expected InvalidPhybSubArrayOverlap, got {other:?}"),
        }
    }

    #[test]
    fn rejects_chain_with_too_many_nodes() {
        let mut buf = Vec::new();
        buf.extend_from_slice(&1u32.to_le_bytes());
        let mut counters = [0u8; SIMULATOR_COUNTER_BLOCK_SIZE];
        counters[2] = 1;
        buf.extend_from_slice(&counters);
        buf.extend_from_slice(&[0u8; SIMULATOR_PARAMS_SIZE]);
        let mut offsets = [0u32; 8];
        offsets[2] = u32::try_from(SIMULATOR_INTRINSIC_SIZE).expect("fits");
        for o in offsets {
            buf.extend_from_slice(&o.to_le_bytes());
        }
        let mut chain = Vec::with_capacity(SIM_CHAIN_INTRINSIC_SIZE);
        chain.extend_from_slice(&0u16.to_le_bytes());
        chain.extend_from_slice(
            &u16::try_from(PHYB_MAX_RECORDS_PER_SECTION + 1)
                .expect("cap + 1 fits u16")
                .to_le_bytes(),
        );
        chain.extend_from_slice(&[0u8; SIM_CHAIN_INTRINSIC_SIZE - 4]);
        assert_eq!(chain.len(), SIM_CHAIN_INTRINSIC_SIZE);
        buf.extend_from_slice(&chain);

        let err = SimulatorData::parse(&buf, 0).unwrap_err();
        match err {
            Error::InvalidPhybCount { kind, .. } => assert_eq!(kind, "chain.num_nodes"),
            other => panic!("expected InvalidPhybCount, got {other:?}"),
        }
    }

    #[test]
    fn simulator_params_all_fields_round_trip() {
        let mut buf = Vec::new();
        buf.extend_from_slice(&1u32.to_le_bytes());
        buf.extend_from_slice(&[0u8; SIMULATOR_COUNTER_BLOCK_SIZE]);
        for v in [1.0f32, -9.8, 0.5] {
            buf.extend_from_slice(&v.to_le_bytes());
        }
        for v in [0.25f32, 0.0, -0.1] {
            buf.extend_from_slice(&v.to_le_bytes());
        }
        buf.extend_from_slice(&12u16.to_le_bytes());
        buf.extend_from_slice(&3u16.to_le_bytes());
        buf.push(SimulatorFlags::FIXED_LENGTH | SimulatorFlags::SIMULATING);
        buf.push(7);
        buf.extend_from_slice(&[0u8; 2]);
        for _ in 0..8 {
            buf.extend_from_slice(&0u32.to_le_bytes());
        }

        let sim_data = SimulatorData::parse(&buf, 0).expect("parse");
        let p = &sim_data.simulators[0].params;
        let bits3 = |arr: [f32; 3]| arr.map(f32::to_bits);
        assert_eq!(bits3(p.gravity), bits3([1.0, -9.8, 0.5]));
        assert_eq!(bits3(p.wind), bits3([0.25, 0.0, -0.1]));
        assert_eq!(p.constraint_loop, 12);
        assert_eq!(p.collision_loop, 3);
        assert!(p.flags.contains(SimulatorFlags::FIXED_LENGTH));
        assert!(p.flags.contains(SimulatorFlags::SIMULATING));
        assert!(!p.flags.contains(SimulatorFlags::COLLISIONS_HANDLED));
        assert_eq!(p.group, 7);
    }

    #[test]
    fn simulator_collision_ref_round_trip() {
        let mut buf = Vec::new();
        buf.extend_from_slice(&1u32.to_le_bytes());
        let mut counters = [0u8; SIMULATOR_COUNTER_BLOCK_SIZE];
        counters[0] = 1;
        buf.extend_from_slice(&counters);
        buf.extend_from_slice(&[0u8; SIMULATOR_PARAMS_SIZE]);
        let mut offsets = [0u32; 8];
        offsets[0] = u32::try_from(SIMULATOR_INTRINSIC_SIZE).expect("intrinsic size fits u32");
        for o in offsets {
            buf.extend_from_slice(&o.to_le_bytes());
        }
        let mut cref = Vec::with_capacity(SIM_COLLISION_REF_SIZE);
        cref.extend_from_slice(&name_buf("col_capsule_1"));
        cref.extend_from_slice(&2i32.to_le_bytes());
        assert_eq!(cref.len(), SIM_COLLISION_REF_SIZE);
        buf.extend_from_slice(&cref);

        let sim_data = SimulatorData::parse(&buf, 0).expect("parse");
        let sim = &sim_data.simulators[0];
        assert_eq!(sim.collision_objects.len(), 1);
        let r = &sim.collision_objects[0];
        assert_eq!(r.collision_name, "col_capsule_1");
        assert_eq!(r.kind, SimulatorCollisionKind::Inside);
    }

    #[test]
    fn connector_round_trip() {
        let mut buf = Vec::new();
        buf.extend_from_slice(&1u32.to_le_bytes());
        let mut counters = [0u8; SIMULATOR_COUNTER_BLOCK_SIZE];
        counters[3] = 1;
        buf.extend_from_slice(&counters);
        buf.extend_from_slice(&[0u8; SIMULATOR_PARAMS_SIZE]);
        let mut offsets = [0u32; 8];
        offsets[3] = u32::try_from(SIMULATOR_INTRINSIC_SIZE).expect("intrinsic size fits u32");
        for o in offsets {
            buf.extend_from_slice(&o.to_le_bytes());
        }
        let mut conn = Vec::with_capacity(SIM_CONNECTOR_SIZE);
        conn.extend_from_slice(&5u16.to_le_bytes());
        conn.extend_from_slice(&6u16.to_le_bytes());
        conn.extend_from_slice(&2u16.to_le_bytes());
        conn.extend_from_slice(&3u16.to_le_bytes());
        conn.extend_from_slice(&0.25f32.to_le_bytes());
        conn.extend_from_slice(&0.1f32.to_le_bytes());
        conn.extend_from_slice(&0.05f32.to_le_bytes());
        conn.extend_from_slice(&1.5f32.to_le_bytes());
        conn.extend_from_slice(&0xDEAD_0001u32.to_le_bytes());
        conn.extend_from_slice(&0x0000_BEEFu32.to_le_bytes());
        assert_eq!(conn.len(), SIM_CONNECTOR_SIZE);
        buf.extend_from_slice(&conn);

        let sim_data = SimulatorData::parse(&buf, 0).expect("parse");
        let sim = &sim_data.simulators[0];
        assert_eq!(sim.connectors.len(), 1);
        let c = &sim.connectors[0];
        assert_eq!(c.chain_id1, 5);
        assert_eq!(c.chain_id2, 6);
        assert_eq!(c.node_id1, 2);
        assert_eq!(c.node_id2, 3);
        assert_eq!(c.collision_radius.to_bits(), 0.25f32.to_bits());
        assert_eq!(c.friction.to_bits(), 0.1f32.to_bits());
        assert_eq!(c.dampening.to_bits(), 0.05f32.to_bits());
        assert_eq!(c.repulsion.to_bits(), 1.5f32.to_bits());
        assert_eq!(c.collision_flag, 0xDEAD_0001);
        assert_eq!(c.continuous_collision_flag, 0x0000_BEEF);
    }

    #[test]
    fn attractor_round_trip() {
        let mut buf = Vec::new();
        buf.extend_from_slice(&1u32.to_le_bytes());
        let mut counters = [0u8; SIMULATOR_COUNTER_BLOCK_SIZE];
        counters[4] = 1;
        buf.extend_from_slice(&counters);
        buf.extend_from_slice(&[0u8; SIMULATOR_PARAMS_SIZE]);
        let mut offsets = [0u32; 8];
        offsets[4] = u32::try_from(SIMULATOR_INTRINSIC_SIZE).expect("intrinsic size fits u32");
        for o in offsets {
            buf.extend_from_slice(&o.to_le_bytes());
        }
        let mut att = Vec::with_capacity(SIM_ATTRACTOR_SIZE);
        att.extend_from_slice(&name_buf("j_elbow"));
        for v in [0.5f32, -0.1, 0.25] {
            att.extend_from_slice(&v.to_le_bytes());
        }
        att.extend_from_slice(&9u16.to_le_bytes());
        att.extend_from_slice(&4u16.to_le_bytes());
        att.extend_from_slice(&0.8f32.to_le_bytes());
        assert_eq!(att.len(), SIM_ATTRACTOR_SIZE);
        buf.extend_from_slice(&att);

        let sim_data = SimulatorData::parse(&buf, 0).expect("parse");
        let sim = &sim_data.simulators[0];
        assert_eq!(sim.attractors.len(), 1);
        let a = &sim.attractors[0];
        assert_eq!(a.bone_name, "j_elbow");
        let bits3 = |arr: [f32; 3]| arr.map(f32::to_bits);
        assert_eq!(bits3(a.bone_offset), bits3([0.5, -0.1, 0.25]));
        assert_eq!(a.chain_id, 9);
        assert_eq!(a.node_id, 4);
        assert_eq!(a.stiffness.to_bits(), 0.8f32.to_bits());
    }

    #[test]
    fn spring_round_trip() {
        let mut buf = Vec::new();
        buf.extend_from_slice(&1u32.to_le_bytes());
        let mut counters = [0u8; SIMULATOR_COUNTER_BLOCK_SIZE];
        counters[6] = 1;
        buf.extend_from_slice(&counters);
        buf.extend_from_slice(&[0u8; SIMULATOR_PARAMS_SIZE]);
        let mut offsets = [0u32; 8];
        offsets[6] = u32::try_from(SIMULATOR_INTRINSIC_SIZE).expect("intrinsic size fits u32");
        for o in offsets {
            buf.extend_from_slice(&o.to_le_bytes());
        }
        let mut spring = Vec::with_capacity(SIM_SPRING_SIZE);
        spring.extend_from_slice(&10u16.to_le_bytes());
        spring.extend_from_slice(&11u16.to_le_bytes());
        spring.extend_from_slice(&0u16.to_le_bytes());
        spring.extend_from_slice(&1u16.to_le_bytes());
        spring.extend_from_slice(&2.5f32.to_le_bytes());
        spring.extend_from_slice(&1.25f32.to_le_bytes());
        assert_eq!(spring.len(), SIM_SPRING_SIZE);
        buf.extend_from_slice(&spring);

        let sim_data = SimulatorData::parse(&buf, 0).expect("parse");
        let sim = &sim_data.simulators[0];
        assert_eq!(sim.springs.len(), 1);
        let s = &sim.springs[0];
        assert_eq!(s.chain_id1, 10);
        assert_eq!(s.chain_id2, 11);
        assert_eq!(s.node_id1, 0);
        assert_eq!(s.node_id2, 1);
        assert_eq!(s.stretch_stiffness.to_bits(), 2.5f32.to_bits());
        assert_eq!(s.compress_stiffness.to_bits(), 1.25f32.to_bits());
    }

    #[test]
    fn post_alignment_round_trip() {
        let mut buf = Vec::new();
        buf.extend_from_slice(&1u32.to_le_bytes());
        let mut counters = [0u8; SIMULATOR_COUNTER_BLOCK_SIZE];
        counters[7] = 1;
        buf.extend_from_slice(&counters);
        buf.extend_from_slice(&[0u8; SIMULATOR_PARAMS_SIZE]);
        let mut offsets = [0u32; 8];
        offsets[7] = u32::try_from(SIMULATOR_INTRINSIC_SIZE).expect("intrinsic size fits u32");
        for o in offsets {
            buf.extend_from_slice(&o.to_le_bytes());
        }
        let mut pa = Vec::with_capacity(SIM_POST_ALIGNMENT_SIZE);
        pa.extend_from_slice(&name_buf("col_capsule_0"));
        pa.extend_from_slice(&2u16.to_le_bytes());
        pa.extend_from_slice(&5u16.to_le_bytes());
        assert_eq!(pa.len(), SIM_POST_ALIGNMENT_SIZE);
        buf.extend_from_slice(&pa);

        let sim_data = SimulatorData::parse(&buf, 0).expect("parse");
        let sim = &sim_data.simulators[0];
        assert_eq!(sim.post_alignments.len(), 1);
        let p = &sim.post_alignments[0];
        assert_eq!(p.collision_name, "col_capsule_0");
        assert_eq!(p.chain_id, 2);
        assert_eq!(p.node_id, 5);
    }

    #[test]
    fn sim_node_all_fields_round_trip() {
        let mut buf = Vec::new();
        buf.extend_from_slice(&1u32.to_le_bytes());
        let mut counters = [0u8; SIMULATOR_COUNTER_BLOCK_SIZE];
        counters[2] = 1;
        buf.extend_from_slice(&counters);
        buf.extend_from_slice(&[0u8; SIMULATOR_PARAMS_SIZE]);
        let mut offsets = [0u32; 8];
        offsets[2] = u32::try_from(SIMULATOR_INTRINSIC_SIZE).expect("fits");
        for o in offsets {
            buf.extend_from_slice(&o.to_le_bytes());
        }

        let node_sub_offset =
            u32::try_from(SIMULATOR_INTRINSIC_SIZE + SIM_CHAIN_INTRINSIC_SIZE).expect("fits");
        let mut chain = Vec::with_capacity(SIM_CHAIN_INTRINSIC_SIZE);
        chain.extend_from_slice(&0u16.to_le_bytes());
        chain.extend_from_slice(&1u16.to_le_bytes());
        chain.extend_from_slice(&[0u8; SIM_CHAIN_INTRINSIC_SIZE - 4 - 4 - 4]);
        chain.extend_from_slice(&0u32.to_le_bytes());
        chain.extend_from_slice(&node_sub_offset.to_le_bytes());
        assert_eq!(chain.len(), SIM_CHAIN_INTRINSIC_SIZE);
        buf.extend_from_slice(&chain);

        let mut node = Vec::with_capacity(SIM_NODE_SIZE);
        node.extend_from_slice(&name_buf("j_hair_tip"));
        node.extend_from_slice(&0.5f32.to_le_bytes());
        node.extend_from_slice(&0.9f32.to_le_bytes());
        node.extend_from_slice(&1.1f32.to_le_bytes());
        node.extend_from_slice(&2.2f32.to_le_bytes());
        node.extend_from_slice(&0.78f32.to_le_bytes());
        for v in [3.0f32, 4.0, 5.0] {
            node.extend_from_slice(&v.to_le_bytes());
        }
        for v in [6.0f32, 7.0, 8.0] {
            node.extend_from_slice(&v.to_le_bytes());
        }
        node.extend_from_slice(&0xABCD_1234u32.to_le_bytes());
        node.extend_from_slice(&0x0001_0002u32.to_le_bytes());
        assert_eq!(node.len(), SIM_NODE_SIZE);
        buf.extend_from_slice(&node);

        let sim_data = SimulatorData::parse(&buf, 0).expect("parse");
        let n = &sim_data.simulators[0].chains[0].nodes[0];
        assert_eq!(n.bone_name, "j_hair_tip");
        assert_eq!(n.radius.to_bits(), 0.5f32.to_bits());
        assert_eq!(n.attract_by_animation.to_bits(), 0.9f32.to_bits());
        assert_eq!(n.wind_scale.to_bits(), 1.1f32.to_bits());
        assert_eq!(n.gravity_scale.to_bits(), 2.2f32.to_bits());
        assert_eq!(n.cone_max_angle.to_bits(), 0.78f32.to_bits());
        let bits3 = |arr: [f32; 3]| arr.map(f32::to_bits);
        assert_eq!(bits3(n.cone_axis_offset), bits3([3.0, 4.0, 5.0]));
        assert_eq!(bits3(n.constraint_plane_normal), bits3([6.0, 7.0, 8.0]));
        assert_eq!(n.collision_flag, 0xABCD_1234);
        assert_eq!(n.continuous_collision_flag, 0x0001_0002);
    }
}
