'use strict';
// SHADES — Lane C — debris: things that should have been tied down.
//
// Hand-rolled kinematic tumble (PLAN3D §5-C.4). No physics engine, no deps.
// Deterministic: step(dt, t), seeded RNG, no Date.now — so a storm replays.
//
// Spawns off `debris` events in the storm JSON, upwind of the yard, and lets the
// wind field carry it across. Drag goes with speed², same as the sail, so the
// same gust that spikes a corner is the one that launches the neighbour's bin.

import * as THREE from '../vendor/three.module.js';
import { rng } from './contracts.js';

const RHO = 1.2;               // air density, kg/m³
const GRAVITY = -9.81;
// Deceleration while resting on the ground, m/s². Drag in a 19 m/s wind gives a
// 9 kg crate ~7 m/s², so it still skitters downwind — which is the whole point.
const GROUND_FRICTION = 3.5;

// Fallback specs for Lane E's debris set (3D-STORE crates/tubs). Radius is the
// collision sphere, not the render bounds — a crate is boxy, but a sphere is
// what you can afford to test 6 of per node per frame.
const MODEL_SPEC = {
  BlueCrate_v2: { r: 0.30, mass: 9, cd: 1.05 },
  BlackTub_v2: { r: 0.34, mass: 5, cd: 1.10 },
  WhiteTub_v2: { r: 0.34, mass: 5, cd: 1.10 },
  WoodenBin_v2: { r: 0.42, mass: 14, cd: 1.05 },
  LibraryTrolley_v1: { r: 0.45, mass: 22, cd: 0.95 },
};
const DEFAULT_SPEC = { r: 0.35, mass: 8, cd: 1.05 };

/**
 * @param {object} o
 * @param {object} o.wind          from weather.js
 * @param {THREE.Object3D} o.scene
 * @param {Object<string,THREE.Object3D>} [o.models]  name -> template (Lane E's GLBs)
 * @param {function} [o.onHitPlayer]  (piece, impact) — Lane D knocks the player down
 * @param {function} [o.onEvent]      (text) — HUD ticker
 * @param {object} [o.bounds]      {x, z} half-extents before despawn
 */
export function createDebris(o = {}) {
  const wind = o.wind;
  const scene = o.scene || null;
  const models = o.models || {};
  const bounds = o.bounds || { x: 26, z: 20 };
  // contracts.js documents world.heightAt as the thing Lane C bounces debris off.
  // Flat fallback so this still runs against a graybox yard.
  const groundAt = o.heightAt || (() => o.groundY ?? 0);
  const rand = rng(((wind && wind.seed) || 1) ^ 0x5eed1e);

  const pieces = [];
  const w = new THREE.Vector3();
  const probe = new THREE.Vector3();

  /** Graybox stand-in so a missing GLB can't break Lane A's merge. */
  function placeholder(spec) {
    const g = new THREE.BoxGeometry(spec.r * 1.8, spec.r * 1.8, spec.r * 1.8);
    const m = new THREE.MeshStandardMaterial({ color: 0x8a6a3a, roughness: 0.9 });
    return new THREE.Mesh(g, m);
  }

  function spawn(ev, t) {
    const spec = { ...(MODEL_SPEC[ev.model] || DEFAULT_SPEC) };
    if (Number.isFinite(ev.mass)) spec.mass = ev.mass;

    // upwind of the yard, offset sideways, so it crosses the whole thing
    const d = wind.dirAt(t);
    const dx = Math.cos(d), dz = Math.sin(d);
    const lat = ev.lateral ?? 0;
    const dist = ev.spawnDist ?? 18;
    const x = -dx * dist - dz * lat;
    const z = -dz * dist + dx * lat;
    const y = groundAt(x, z) + spec.r + (ev.height ?? 0.2 + rand() * 1.2);

    const tmpl = models[ev.model];
    const mesh = tmpl ? tmpl.clone(true) : placeholder(spec);
    mesh.castShadow = true;
    if (scene) scene.add(mesh);

    // already moving — it's been blowing across the neighbour's yard for a while
    probe.set(x, y, z);
    wind.sample(probe, t, w);

    const piece = {
      model: ev.model,
      x, y, z,
      vx: w.x * 0.8, vy: 0, vz: w.z * 0.8,
      // spin axis is arbitrary but seeded; rate scales with airspeed in step()
      sx: rand() * 2 - 1, sy: rand() * 2 - 1, sz: rand() * 2 - 1,
      spin: 0,
      phase: rand() * 6.283,       // so two crates don't hop in lockstep
      r: spec.r, mass: spec.mass, cd: spec.cd,
      area: Math.PI * spec.r * spec.r,
      hitPlayer: false,
      mesh,
      alive: true,
    };
    pieces.push(piece);
    if (ev.text && o.onEvent) o.onEvent(ev.text);
    return piece;
  }

  function despawn(p) {
    p.alive = false;
    if (scene && p.mesh) scene.remove(p.mesh);
  }

  const debris = {
    get pieces() { return pieces; },

    /** Lane E's GLBs, once they land. name -> Object3D template. */
    setModels(map) { Object.assign(models, map); return debris; },

    /** Manual spawn — handy for tuning and for Lane A's debug keys. */
    spawn,

    /**
     * @param {number} dt fixed step
     * @param {number} t  storm time
     * @param {object} [world] {player, sail} — both optional, both duck-typed
     */
    step(dt, t, world = {}) {
      // storm JSON drives the spawns; poll the window so nothing is missed
      if (wind) {
        for (const ev of wind.eventsBetween(t - dt, t)) {
          if (ev.type === 'debris') spawn(ev, t);
        }
      }

      const player = world.player;
      const sail = world.sail;

      for (let i = pieces.length - 1; i >= 0; i--) {
        const p = pieces[i];

        probe.set(p.x, p.y, p.z);
        wind.sample(probe, t, w);

        // drag against the AIR, not the ground: F = ½ρ Cd A |w-v| (w-v)
        const rx = w.x - p.vx, ry = w.y - p.vy, rz = w.z - p.vz;
        const rel = Math.hypot(rx, ry, rz);
        const k = 0.5 * RHO * p.cd * p.area * rel / p.mass;
        p.vx += rx * k * dt;
        p.vy += ry * k * dt + GRAVITY * dt;
        p.vz += rz * k * dt;

        // A tumbling bluff body doesn't just get shoved, it gets picked up: lift
        // flips sign as it rolls, which is why a bin HOPS across a yard instead
        // of sliding. Wind is horizontal (weather.js keeps y=0), so without this
        // there is no vertical force at all once it's down and it just skates.
        p.vy += (0.5 * rel * rel * Math.sin(p.spin * 1.7 + p.phase) / p.mass) * dt;

        p.x += p.vx * dt;
        p.y += p.vy * dt;
        p.z += p.vz * dt;

        // ground
        const floor = groundAt(p.x, p.z) + p.r;
        if (p.y <= floor) {
          p.y = floor;
          if (p.vy < -0.5) {
            // a real impact: bounce, and lose some tangential speed to the hit
            p.vy = -p.vy * 0.32;                       // dead-ish, it's a plastic tub
            p.vx *= 0.72; p.vz *= 0.72;
          } else {
            if (p.vy < 0) p.vy = 0;
            // Resting: rolling friction as a dt-scaled DECELERATION, not a
            // per-frame multiplier. `v *= 0.86` every frame is 0.86^60 per
            // second — that isn't scrape, it's glue, and it pinned a 9 kg crate
            // at 0.7 m/s in a 19 m/s wind.
            const sp = Math.hypot(p.vx, p.vz);
            if (sp > 1e-4) {
              const drop = Math.min(sp, GROUND_FRICTION * dt);
              p.vx -= (p.vx / sp) * drop;
              p.vz -= (p.vz / sp) * drop;
            }
          }
        }

        // tumble rate follows airspeed — becalmed debris shouldn't keep spinning
        p.spin += rel * 0.35 * dt;
        if (p.mesh) {
          p.mesh.position.set(p.x, p.y, p.z);
          p.mesh.rotation.set(p.sx * p.spin, p.sy * p.spin, p.sz * p.spin);
        }

        // --- sphere vs player: knockdown ---
        // Contract gives us player.pos; the knockdown itself is Lane D's (§5-D.3),
        // so we just report the hit and let them run the state machine.
        if (player && player.pos && !p.hitPlayer) {
          const px = player.pos.x, pz = player.pos.z;
          const py = player.pos.y + 0.9;                // centre of mass, not feet
          const dsq = (p.x - px) ** 2 + (p.y - py) ** 2 + (p.z - pz) ** 2;
          const hit = p.r + 0.35;
          if (dsq < hit * hit) {
            const impact = Math.hypot(p.vx, p.vy, p.vz) * p.mass;
            // a bin rolling gently past your ankles shouldn't floor you
            if (impact > 25 && o.onHitPlayer) {
              p.hitPlayer = true;                      // one knockdown per piece
              o.onHitPlayer(p, impact);
              p.vx *= 0.4; p.vz *= 0.4;
            }
          }
        }

        // --- sphere vs sail nodes: impulse ---
        // Duck-typed: lights up the moment Lane B exposes nodes, silent until
        // then. See THREADS — B owns sail.js, so this is the seam we agreed on.
        if (sail && sail.nodes) applyToSail(p, sail);

        if (p.y < groundAt(p.x, p.z) - 5 || Math.abs(p.x) > bounds.x || Math.abs(p.z) > bounds.z) {
          despawn(p);
          pieces.splice(i, 1);
        }
      }
    },

    /** Drop everything (phase change, restart). */
    clear() {
      for (const p of pieces) despawn(p);
      pieces.length = 0;
    },
  };

  /**
   * Shove any cloth node the piece is intersecting, and lose some of the piece's
   * own momentum doing it. Expects sail.nodes: [{x,y,z,px,py,pz}] (verlet, so we
   * move position and let the integrator turn it into velocity).
   */
  function applyToSail(p, sail) {
    const nodes = sail.nodes;
    const reach = p.r + 0.15;
    const reachSq = reach * reach;
    let hits = 0;
    for (let i = 0; i < nodes.length; i++) {
      const n = nodes[i];
      const dx = n.x - p.x, dy = n.y - p.y, dz = n.z - p.z;
      const dsq = dx * dx + dy * dy + dz * dz;
      if (dsq > reachSq || dsq < 1e-9) continue;
      const d = Math.sqrt(dsq);
      // push the node out to the sphere surface along the contact normal
      const push = (reach - d) / d;
      n.x += dx * push; n.y += dy * push; n.z += dz * push;
      hits++;
    }
    if (hits) {
      const drag = Math.min(0.5, (hits * p.mass) / 400);
      p.vx *= 1 - drag; p.vy *= 1 - drag; p.vz *= 1 - drag;
      if (sail.onDebrisHit) sail.onDebrisHit(p, hits);
    }
  }

  return debris;
}
