//! `wai.world.sim.truss0` — the solver-backed registrant.
//!
//! Structural simulation: point-mass particles joined by distance
//! members, solved per tick by **position-based dynamics** — a
//! fixed-count Gauss–Seidel projection over the constraint set, in
//! canonical member order, entirely in `wai.det.fixed64`. Members
//! carry a yield strain; a member whose constraint violation exceeds
//! it **fractures** — removed from state before the solve, so an
//! overloaded structure fails the way the solver says it fails, on
//! every machine, bit-for-bit.
//!
//! This registrant is where engineering-solver thinking enters the
//! registry: stress is computed, not animated, and structural failure
//! is an emergent consequence of load — including the load two actors
//! apply by pulling the same member in opposite directions (the SUM
//! conflict rule feeding the solver). Like every sim capability it
//! entered by registration: the extension is unchanged.
//!
//! Registration constants (fixed forever): gravity `G = −10` on y,
//! `ITERATIONS = 8` solver passes per tick, ground plane at `y = 0`,
//! velocity damping `1/64` per tick (structures reach static
//! equilibrium — a hanging mass settles instead of swinging forever,
//! which is what makes "the structure holds" a checkable state).
//!
//! Determinism notes: the solve visits members in ascending node-id
//! order (canonical iteration of the state map); strain is measured on
//! predicted positions BEFORE the solve (the constraint violation the
//! applied load produces — measuring after projection would hide the
//! stress the solver just absorbed); all arithmetic is `Fx`, including
//! the distance square root ([`Fx::sqrt`], floor-rounded).

use super::fixed::Fx;
use super::graph::WorldState;
use super::op::{mint_node_id, CausalOp};
use super::rng::Xoshiro256pp;
use super::sim::{Resolve, Sim};
use super::{SimContract, WorldError};

pub const CAP_SIM_TRUSS0: &str = "wai.world.sim.truss0";

/// Mutation kinds registered by truss0. Numbering mirrors the other
/// registrants where semantics coincide.
pub mod truss_mutation {
    /// Create a particle: `pos[3] | vel[3] | inv_mass` (7 × i64).
    /// `inv_mass == 0` is an anchor (infinite mass — foundations).
    /// Target MUST equal `mint_node_id(actor, lamport)`. LWW.
    pub const CREATE_PARTICLE: u16 = 0x0001;
    /// Empty payload; removes a particle or member. LWW.
    pub const DELETE: u16 = 0x0002;
    /// `dv[3]` (3 × i64) added to a particle's velocity. SUM.
    pub const IMPULSE: u16 = 0x0004;
    /// `pos[3]` (3 × i64) on a particle. LWW.
    pub const SET_POS: u16 = 0x0005;
    /// Create a member between two particles:
    /// `node_a (16 B) | node_b (16 B) | yield_strain (i64)`.
    /// Rest length is measured from the endpoints' current positions
    /// at apply time (weld what's there — deterministic, since apply
    /// order is canonical). Target MUST be minted; endpoints missing
    /// or coincident → deterministic no-op. LWW.
    pub const CREATE_MEMBER: u16 = 0x0010;
}

/// Truss state: a node is a particle or a member. Canonical
/// serialization is a kind tag then the fields in struct order.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum TrussNode {
    Particle {
        pos: [Fx; 3],
        vel: [Fx; 3],
        /// Inverse mass; 0 = anchored.
        inv_mass: Fx,
    },
    Member {
        a: [u8; 16],
        b: [u8; 16],
        rest: Fx,
        yield_strain: Fx,
    },
}

pub struct Truss0;

impl Truss0 {
    const GRAVITY: Fx = Fx::from_int(-10);
    const ITERATIONS: u32 = 8;
    /// Per-tick velocity retention: `1 − 1/64` in Q32.32 raw bits.
    const DAMPING: Fx = Fx::from_bits((1i64 << 32) - (1i64 << 26));
}

impl Sim for Truss0 {
    type Node = TrussNode;

    fn capability(&self) -> &'static str {
        CAP_SIM_TRUSS0
    }

    fn resolve_kind(&self, mutation_kind: u16) -> Option<Resolve> {
        match mutation_kind {
            truss_mutation::CREATE_PARTICLE
            | truss_mutation::DELETE
            | truss_mutation::SET_POS
            | truss_mutation::CREATE_MEMBER => Some(Resolve::Lww),
            truss_mutation::IMPULSE => Some(Resolve::Sum),
            _ => None,
        }
    }

    fn apply(&self, world: &mut WorldState<TrussNode>, op: &CausalOp) -> Result<(), WorldError> {
        match op.mutation_kind {
            truss_mutation::CREATE_PARTICLE => {
                if op.target != mint_node_id(&op.actor, op.lamport) {
                    return Err(WorldError::BadNodeMint);
                }
                let v = take_fx::<7>(&op.mutation)?;
                world.nodes.insert(
                    op.target,
                    TrussNode::Particle {
                        pos: [v[0], v[1], v[2]],
                        vel: [v[3], v[4], v[5]],
                        inv_mass: v[6],
                    },
                );
            }
            truss_mutation::DELETE => {
                if !op.mutation.is_empty() {
                    return Err(WorldError::BadMutation);
                }
                world.nodes.remove(&op.target);
            }
            truss_mutation::IMPULSE => {
                let v = take_fx::<3>(&op.mutation)?;
                if let Some(TrussNode::Particle { vel, .. }) = world.nodes.get_mut(&op.target) {
                    for (w, dv) in vel.iter_mut().zip(v) {
                        *w = *w + dv;
                    }
                }
            }
            truss_mutation::SET_POS => {
                let v = take_fx::<3>(&op.mutation)?;
                if let Some(TrussNode::Particle { pos, .. }) = world.nodes.get_mut(&op.target) {
                    *pos = [v[0], v[1], v[2]];
                }
            }
            truss_mutation::CREATE_MEMBER => {
                if op.target != mint_node_id(&op.actor, op.lamport) {
                    return Err(WorldError::BadNodeMint);
                }
                if op.mutation.len() != 16 + 16 + 8 {
                    return Err(WorldError::BadMutation);
                }
                let a: [u8; 16] = op.mutation[..16].try_into().unwrap();
                let b: [u8; 16] = op.mutation[16..32].try_into().unwrap();
                let yield_strain =
                    Fx::from_bits(i64::from_le_bytes(op.mutation[32..40].try_into().unwrap()));
                // Weld what's there: rest length measured now. Missing
                // or coincident endpoints → deterministic no-op.
                let (pa, pb) = match (particle_pos(world, &a), particle_pos(world, &b)) {
                    (Some(pa), Some(pb)) => (pa, pb),
                    _ => return Ok(()),
                };
                let rest = distance(pa, pb);
                if rest == Fx::ZERO {
                    return Ok(());
                }
                world.nodes.insert(op.target, TrussNode::Member { a, b, rest, yield_strain });
            }
            k => return Err(WorldError::UnknownMutationKind(k)),
        }
        Ok(())
    }

    fn step(
        &self,
        world: &mut WorldState<TrussNode>,
        contract: &SimContract,
        _tick: u64,
        _rng: &mut Xoshiro256pp,
    ) {
        use std::collections::BTreeMap;
        let dt = Fx::ONE / Fx::from_int(contract.tick_rate as i32);
        let rate = Fx::from_int(contract.tick_rate as i32);

        // 1. Gravity + position prediction (particles, canonical order).
        let mut pred: BTreeMap<[u8; 16], ([Fx; 3], [Fx; 3], Fx)> = BTreeMap::new(); // id → (old_pos, pred_pos, inv_mass)
        for (id, node) in world.nodes.iter_mut() {
            if let TrussNode::Particle { pos, vel, inv_mass } = node {
                if *inv_mass > Fx::ZERO {
                    vel[1] = vel[1] + Self::GRAVITY * dt;
                    for v in vel.iter_mut() {
                        *v = *v * Self::DAMPING;
                    }
                }
                let p = [pos[0] + vel[0] * dt, pos[1] + vel[1] * dt, pos[2] + vel[2] * dt];
                pred.insert(*id, (*pos, p, *inv_mass));
            }
        }

        // 2. Strain check on PREDICTED positions, before the solve: a
        //    member whose constraint violation exceeds its yield strain
        //    fractures NOW and does not constrain this tick. Members
        //    with a missing endpoint also break.
        struct MemberRef {
            id: [u8; 16],
            a: [u8; 16],
            b: [u8; 16],
            rest: Fx,
            yield_strain: Fx,
        }
        let members: Vec<MemberRef> = world
            .nodes
            .iter()
            .filter_map(|(id, n)| match n {
                TrussNode::Member { a, b, rest, yield_strain } => Some(MemberRef {
                    id: *id,
                    a: *a,
                    b: *b,
                    rest: *rest,
                    yield_strain: *yield_strain,
                }),
                _ => None,
            })
            .collect();
        let mut broken: Vec<[u8; 16]> = Vec::new();
        let mut intact: Vec<([u8; 16], [u8; 16], Fx)> = Vec::new(); // (a, b, rest)
        for m in &members {
            match (pred.get(&m.a), pred.get(&m.b)) {
                (Some((_, pa, _)), Some((_, pb, _))) => {
                    let dist = distance(*pa, *pb);
                    let violation = if dist >= m.rest { dist - m.rest } else { m.rest - dist };
                    if violation / m.rest > m.yield_strain {
                        broken.push(m.id);
                    } else {
                        intact.push((m.a, m.b, m.rest));
                    }
                }
                _ => broken.push(m.id),
            }
        }
        for id in &broken {
            world.nodes.remove(id);
        }

        // 3. Fixed-count Gauss–Seidel projection over intact members,
        //    canonical member order (the Vec preserves it).
        for _ in 0..Self::ITERATIONS {
            for (a, b, rest) in &intact {
                let (pa, wa) = { let e = &pred[a]; (e.1, e.2) };
                let (pb, wb) = { let e = &pred[b]; (e.1, e.2) };
                let wsum = wa + wb;
                if wsum == Fx::ZERO {
                    continue; // two anchors
                }
                let d = [pb[0] - pa[0], pb[1] - pa[1], pb[2] - pa[2]];
                let dist = distance(pa, pb);
                if dist == Fx::ZERO {
                    continue;
                }
                let c = dist - *rest; // signed violation
                let scale = c / dist;
                let corr = [d[0] * scale, d[1] * scale, d[2] * scale];
                let fa = wa / wsum;
                let fb = wb / wsum;
                {
                    let e = pred.get_mut(a).unwrap();
                    for (p, c) in e.1.iter_mut().zip(corr) {
                        *p = *p + c * fa;
                    }
                }
                {
                    let e = pred.get_mut(b).unwrap();
                    for (p, c) in e.1.iter_mut().zip(corr) {
                        *p = *p - c * fb;
                    }
                }
            }
        }

        // 4. Ground plane at y = 0.
        for e in pred.values_mut() {
            if e.1[1] < Fx::ZERO {
                e.1[1] = Fx::ZERO;
            }
        }

        // 5. Velocity from positions, write back.
        for (id, (old, new, _w)) in pred {
            if let Some(TrussNode::Particle { pos, vel, .. }) = world.nodes.get_mut(&id) {
                for i in 0..3 {
                    vel[i] = (new[i] - old[i]) * rate;
                }
                *pos = new;
            }
        }
    }

    fn serialize_node(&self, node: &TrussNode, out: &mut Vec<u8>) {
        match node {
            TrussNode::Particle { pos, vel, inv_mass } => {
                out.push(0);
                for v in pos.iter().chain(vel.iter()).chain([inv_mass]) {
                    out.extend_from_slice(&v.to_bits().to_le_bytes());
                }
            }
            TrussNode::Member { a, b, rest, yield_strain } => {
                out.push(1);
                out.extend_from_slice(a);
                out.extend_from_slice(b);
                out.extend_from_slice(&rest.to_bits().to_le_bytes());
                out.extend_from_slice(&yield_strain.to_bits().to_le_bytes());
            }
        }
    }

    fn parse_node(&self, bytes: &[u8]) -> Result<TrussNode, WorldError> {
        match bytes.first() {
            Some(0) if bytes.len() == 1 + 7 * 8 => {
                let mut v = [Fx::ZERO; 7];
                for (i, chunk) in bytes[1..].chunks_exact(8).enumerate() {
                    v[i] = Fx::from_bits(i64::from_le_bytes(chunk.try_into().unwrap()));
                }
                Ok(TrussNode::Particle {
                    pos: [v[0], v[1], v[2]],
                    vel: [v[3], v[4], v[5]],
                    inv_mass: v[6],
                })
            }
            Some(1) if bytes.len() == 1 + 16 + 16 + 16 => {
                Ok(TrussNode::Member {
                    a: bytes[1..17].try_into().unwrap(),
                    b: bytes[17..33].try_into().unwrap(),
                    rest: Fx::from_bits(i64::from_le_bytes(bytes[33..41].try_into().unwrap())),
                    yield_strain: Fx::from_bits(i64::from_le_bytes(
                        bytes[41..49].try_into().unwrap(),
                    )),
                })
            }
            _ => Err(WorldError::BadMutation),
        }
    }
}

fn particle_pos(world: &WorldState<TrussNode>, id: &[u8; 16]) -> Option<[Fx; 3]> {
    match world.nodes.get(id) {
        Some(TrussNode::Particle { pos, .. }) => Some(*pos),
        _ => None,
    }
}

fn distance(a: [Fx; 3], b: [Fx; 3]) -> Fx {
    let d = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
    (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
}

/// Payload helper: encode a CREATE_MEMBER mutation.
pub fn pack_member(a: &[u8; 16], b: &[u8; 16], yield_strain: Fx) -> Vec<u8> {
    let mut out = Vec::with_capacity(40);
    out.extend_from_slice(a);
    out.extend_from_slice(b);
    out.extend_from_slice(&yield_strain.to_bits().to_le_bytes());
    out
}

fn take_fx<const N: usize>(bytes: &[u8]) -> Result<[Fx; N], WorldError> {
    if bytes.len() != N * 8 {
        return Err(WorldError::BadMutation);
    }
    let mut out = [Fx::ZERO; N];
    for (i, chunk) in bytes.chunks_exact(8).enumerate() {
        out[i] = Fx::from_bits(i64::from_le_bytes(chunk.try_into().unwrap()));
    }
    Ok(out)
}
