/**
 * SHADES — shared contracts. THE integration spine.
 *
 * Owner: Lane A. This file changes ONLY by agreement — if you need a new shape,
 * post the need in THREADS.md and let Lane A land it. Everything else in the
 * game talks through the types and constants here.
 *
 * House rules encoded in this file:
 *  - Units are meters, +Y is up, world origin is yard centre on the ground.
 *    North is -Z (the house edge), south is +Z, east is +X.
 *  - Sim modules (sail.js, weather.js, debris.js) take (dt, t) and are pure in
 *    those: no Date.now(), no Math.random(), no rAF. selftest.html fast-forwards
 *    storms by calling step() in a tight fixed-dt loop, which only works if the
 *    sim is deterministic. Use rng() below when you need randomness.
 */

import * as THREE from '../vendor/three.module.js';

// ---------------------------------------------------------------------------
// Units & tuning constants
// ---------------------------------------------------------------------------

/** Sim tick. Every sim module steps at exactly this dt. */
export const FIXED_DT = 1 / 60;

/** Yard footprint in meters. x spans ±WIDTH/2, z spans ±DEPTH/2. */
export const YARD = { width: 30, depth: 20 };

/** Storm length in seconds (prototype value — storm JSON may override). */
export const STORM_LEN = 90;

/** Prep budget in dollars (prototype value). */
export const START_BUDGET = 80;

/** Cost of one spare shackle carried into the storm. */
export const SPARE_COST = 15;

/**
 * Hardware tiers, ported from prototype/game.js.
 *
 * `rating` is a working load limit in NEWTONS — retuned by Lane B against the
 * 3D cloth's real load output, per the standing note that Lane B owns these
 * numbers. Costs are the prototype's, untouched.
 *
 * The 2D prototype's 9/19/40 were on an arbitrary scale. The 3D cloth reports
 * real newtons (a 5x5 m sail pulls ~1-4 kN per corner in a 34 m/s storm), so
 * these are real WLLs: a cheap carabiner really does let go around 1.2 kN, a
 * rated 8 mm shackle really does hold 6.5 kN. That is the DESIGN.md "Kerbal
 * trick" — leave the game able to size real hardware.
 *
 * The SHAPE that had to survive retuning, and did: three tiers at 1x / 3x / 6x
 * price, where $80 buys rated hardware on at most two of four corners. A mixed
 * rig stays the interesting choice and you are always picking which corner to
 * leave dodgy. Asserted in js/tests/b.test.js.
 */
export const HARDWARE = [
  { name: 'carabiner',     cost: 5,  rating: 1200, color: 0xe2b04a },
  { name: 'shackle',       cost: 15, rating: 3200, color: 0xc8d2d8 },
  { name: 'rated shackle', cost: 30, rating: 6500, color: 0x7ee0ff },
];

/** Game phases, in loop order. */
export const PHASES = ['forecast', 'prep', 'storm', 'aftermath'];

/**
 * Every anchor type a site may declare (SPRINT11, D's flag).
 *
 * These are the strings Lane E bakes as `anchor_type` in the GLBs — kept
 * identical on purpose, so an asset and a site say the same word for the same
 * steel. The list is OPEN to new sites: adding a type here and in the asset is
 * the intended way to grow, which is the whole reason the carport didn't fit.
 *
 * The only behaviour keyed on a type string is the tree wind-shadow filter
 * (main.js: `type === 'tree'`). Ladder work is `work`; hardware rating is E's
 * `rating_hint`. Keep it that way — a rule keyed on a type a future site
 * doesn't fit is the exact shape of the bug this widening fixed.
 *
 * `swing_frame` (SPRINT14, E): a swing set's apex junction. It is NOT a `post`
 * and the difference is the whole reason it got its own word — the type string
 * is what the player reads before they commit, and "post" promises 4 m of
 * concreted steel. This is a welded joint on a frame standing on grass: good
 * steel, no footing, rating_hint 0.45. Same lesson as the carport, which was
 * also nearly smuggled in as a post.
 */
export const ANCHOR_TYPE = Object.freeze([
  'house', 'tree', 'post', 'carport', 'carport_post', 'swing_frame',
]);

// ---------------------------------------------------------------------------
// Determinism helpers
// ---------------------------------------------------------------------------

/**
 * mulberry32 — small, fast, seeded PRNG. Use this instead of Math.random() in
 * any sim code so selftest runs reproduce byte-for-byte.
 * @param {number} seed
 * @returns {() => number} next float in [0,1)
 */
export function rng(seed) {
  let a = seed >>> 0;
  return function () {
    a |= 0; a = (a + 0x6D2B79F5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

/** Tiny event emitter. Backs `game.on()` and `sailRig.events`. */
export class Emitter {
  #handlers = new Map();

  /**
   * @param {string} type
   * @param {(payload:any) => void} fn
   * @returns {() => void} unsubscribe
   */
  on(type, fn) {
    if (!this.#handlers.has(type)) this.#handlers.set(type, new Set());
    this.#handlers.get(type).add(fn);
    return () => this.off(type, fn);
  }

  off(type, fn) {
    this.#handlers.get(type)?.delete(fn);
  }

  emit(type, payload) {
    for (const fn of this.#handlers.get(type) ?? []) fn(payload);
  }
}

// ---------------------------------------------------------------------------
// The contracts themselves
// ---------------------------------------------------------------------------

/**
 * WIND — the shared primitive. weather.js (Lane C) implements it; world, sail,
 * player and debris all consume it. Everything that moves in this game moves
 * because of a number that came out of wind.sample().
 *
 * @typedef {object} Wind
 * @property {(pos: THREE.Vector3, t: number) => THREE.Vector3} sample
 *   Wind velocity in m/s at a world position and time. Includes base curve,
 *   gusts, direction change, spatial noise and local effects (tree wind
 *   shadows). MUST be pure in (pos, t). Returns a vector the caller may not
 *   mutate — treat it as read-only, or clone before writing.
 * @property {(t: number) => ({eta:number, dir:number, power:number}|null)} gustTelegraph
 *   The gust that is coming but has not hit yet, or null. `eta` is seconds
 *   until the ramp starts, `dir` is radians in the XZ plane, `power` is the
 *   peak speed in m/s the gust will add. Drives the HUD warning, the grass
 *   wave and the audio cue. Contract: eta is never less than 1.2 s when the
 *   telegraph first appears — the player must always have time to react.
 */

/**
 * WORLD — the yard. Lane A implements; everyone consumes.
 *
 * @typedef {object} World
 * @property {Anchor[]} anchors             Every riggable point in the yard.
 * @property {(x:number, z:number) => number} heightAt
 *   Ground height in meters at a world XZ. Pure, cheap, matches the terrain
 *   mesh. Lane D clamps the player to it; Lane C bounces debris off it.
 * @property {{x:number, z:number, w:number, d:number}} gardenBed
 *   The thing you are protecting: an axis-aligned ground rect, centre (x,z),
 *   size (w,d) in meters. Pass it to sailRig.coverageOver().
 * @property {THREE.Vector3} sunDir
 *   Unit vector pointing FROM the ground TOWARD the sun. For a shade test,
 *   raycast origin=groundPoint direction=sunDir: a hit means shaded.
 * @property {THREE.Object3D[]} solids
 *   Obstacle meshes the camera and player collide against: house, trunks,
 *   posts, fence. The GROUND IS NOT IN HERE — use heightAt() for that. It is
 *   exact, it can't be tunnelled through, and it costs nothing.
 * @property {(dt:number, t:number) => void} update
 */

/**
 * ANCHOR — a tower slot. Where a sail corner can be attached.
 *
 * @typedef {object} Anchor
 * @property {string} id                    Stable, e.g. 'h1', 't1', 'p2'.
 * @property {THREE.Vector3} pos            REST position. Does not move.
 * @property {ANCHOR_TYPE[number]} type
 *   What the anchor IS. Widened in SPRINT11 on D's flag: site_02's carport was
 *   typed 'post' to fit the old closed enum, which quietly enrolled it in the
 *   sail-post family that C's venturi and B's audit both read. The types now
 *   match the strings Lane E bakes into the GLB, so `type` can be believed.
 *   NOT the ladder's field — that's `work`, the mechanism (D, SPRINT10).
 * @property {(t:number) => THREE.Vector3} sway
 *   ABSOLUTE world position at time t, including wind sway. NOT an offset —
 *   this is the value you pin a cloth corner to:
 *       node.copy(anchor.sway(t))
 *   For house and post anchors this equals `pos`. For tree anchors it wanders,
 *   and that wander is dynamic load — the reason tree anchors are dangerous.
 *   Pure in t (given wind is pure in t). Returns a shared vector: clone before
 *   storing.
 */

/**
 * SAIL RIG — Lane B implements.
 *
 * @typedef {object} SailRig
 * @property {Corner[]} corners
 * @property {(anchorIds: string[], hwChoices: object[], tension: number) => void} attach
 *   anchorIds has exactly 4 entries; the rig re-orders them into ring order by
 *   angle around their centroid. tension scales spring rest lengths, 0.6–1.4
 *   (low = loose and floggy, high = drum tight and shock-loaded).
 * @property {(dt:number, wind:Wind, t:number) => void} step  Fixed dt. Deterministic.
 * @property {(rect: {x:number,z:number,w:number,d:number}, sunDir?: THREE.Vector3, heightAt?: (x:number,z:number)=>number) => number} coverageOver
 *   Ground-projected shade over a rect, 0..1. Pass world.sunDir and
 *   world.heightAt so the rays start at the real ground and point at the real
 *   sun; the defaults (overhead sun, flat y=0) are only for tests.
 * @property {Emitter} events               Emits 'break' and 'repair' as {type, corner}.
 * @property {(i: number) => void} repair
 *   Re-rig corner i with the carried spare (shackle grade — the only kind prep
 *   sells). No-op if the corner isn't broken. Lane D's 2.5 s hold-E.
 * @property {(i: number, delta: number) => void} trim
 *   Per-corner turnbuckle; delta is ±, clamped to 0.85–1.15. Lane D's 1.2 s hold.
 * @property {(i: number) => (THREE.Vector3|null)} cornerPos
 *   LIVE world position of corner i, as a fresh vector safe to keep. A blown
 *   corner's node is flying, so an interaction prompt anchored to this chases
 *   the flogging corner instead of sitting on the dead anchor. null if unrigged.
 * @property {() => number} pondMass
 *   Kilograms of rainwater pooled on the sail (SPRINT5 ponding). Lane A's HUD
 *   warning threshold and "SAIL PONDING — get the broom" ticker read this.
 * @property {() => ({x:number,y:number,z:number,mass:number,node:number}|null)} pondCentroid
 *   Where the pond sits in world space, its mass, and the heaviest grid node —
 *   or null if there's nothing worth pointing at. Lane D walks the player to
 *   this; Lane E draws the water here.
 * @property {(node:number, dt:number, radius?:number) => number} drainPondAt
 *   Lane D's broom: poke node `node` (from pondCentroid().node) for one frame of
 *   the ~1.5 s hold; drains a radius around it and RETURNS the kg shed this call.
 *   Sum over the hold = what lands on the player's head. Emits 'pondDump'.
 */

/**
 * @typedef {object} Corner
 * @property {string} anchorId
 * @property {{name:string, cost:number, rating:number}} hw
 * @property {number} load                  Smoothed, same units as hw.rating.
 * @property {boolean} broken
 */

/**
 * DEBRIS — Lane C implements. Lane B consumes `pieces` inside sail.step().
 *
 * SPRINT2 decision 5: the sail reads the pieces and applies its own impulses,
 * rather than debris.js reaching into the cloth. Momentum bookkeeping stays in
 * the one integrator that owns the nodes. That makes `pieces` a real contract
 * surface, so it is **frozen** here: fields below are what Lane B may rely on.
 *
 * @typedef {object} Debris
 * @property {DebrisPiece[]} pieces
 *   Live pieces, newest last. The ARRAY IS MUTATED IN PLACE each step — pieces
 *   are spliced out when they leave the yard, so don't hold a reference to it
 *   across frames, and don't hold a piece past the step it despawned in. Read it
 *   fresh inside step(). Order is not stable.
 * @property {(dt:number, t:number, world?:object) => void} step  Fixed dt. Deterministic.
 * @property {(ev:object, t:number) => DebrisPiece} spawn
 * @property {(map:Object<string,THREE.Object3D>) => Debris} setModels
 * @property {() => void} clear
 */

/**
 * One airborne object. Frozen shape — Lane C will not remove or repurpose these.
 *
 * The collision volume is a SPHERE of radius `r` centred on (x,y,z): a crate is
 * boxy, but a sphere is what you can afford to test against every cloth node,
 * every frame. Everything is SI — metres, m/s, kg — so `mass * v` is a real
 * momentum you can subtract from.
 *
 * @typedef {object} DebrisPiece
 * @property {number} x
 * @property {number} y                     Centre, not base. Rests at heightAt(x,z) + r.
 * @property {number} z
 * @property {number} vx
 * @property {number} vy
 * @property {number} vz
 * @property {number} r                     Collision sphere radius, m.
 * @property {number} mass                  kg. Crate 9, tub 5, bin 14.
 * @property {string} model                 Key into models/debris/, e.g. 'BlueCrate_v2'.
 * @property {boolean} hitPlayer            Already knocked the player down once.
 * @property {THREE.Object3D|null} mesh     Render instance. Lane C drives it; don't move it.
 */

/**
 * PLAYER — Lane D implements.
 *
 * @typedef {object} Player
 * @property {THREE.Vector3} pos
 * @property {object|null} carrying         One item, or null. Hands-full rule.
 * @property {boolean} busy                 True during a hold-E action or knockdown.
 * @property {(dt:number, t:number) => void} update
 *   Driven by main.js's fixed-dt loop. (Lane A addition to PLAN3D §4, which
 *   listed only the read-only fields — main.js has to step the player from
 *   somewhere. Logged in THREADS.md.)
 */

/**
 * INTERACT — Lane D implements. Lane B and E register the touchable things.
 *
 * @typedef {object} Interact
 * @property {(spec: InteractSpec) => (() => void)} register  Returns an unregister fn.
 */

/**
 * @typedef {object} InteractSpec
 * @property {string} id
 * @property {THREE.Vector3} pos
 * @property {number} radius                Meters.
 * @property {number} holdSecs              0 for instant.
 * @property {string} label                 Shown in the prompt, e.g. 're-rig corner'.
 * @property {() => boolean} canUse         Gate on spares, busy, carrying.
 * @property {() => void} onDone
 */

/**
 * CAMERA — Lane A implements. Lane D needs `yaw` for camera-relative movement.
 *
 * @typedef {object} CameraRig
 * @property {THREE.PerspectiveCamera} object
 * @property {number} yaw                   Radians. WASD is relative to this.
 * @property {(dt:number, targetPos:THREE.Vector3) => void} update
 */

/**
 * GAME — Lane A implements.
 *
 * @typedef {object} Game
 * @property {'forecast'|'prep'|'storm'|'aftermath'} phase
 * @property {(type:'phaseChange', fn:(p:{from:string,to:string}) => void) => (() => void)} on
 *   NOTE: PLAN3D §4 writes this as `game.on(phaseChange)`. It is implemented as
 *   Emitter-style `on(type, fn)` with type 'phaseChange', for consistency with
 *   sailRig.events. (Lane A clarification, logged in THREADS.md.)
 */

// ---------------------------------------------------------------------------
// Shape checking — used by selftest to catch contract drift at merge time
// ---------------------------------------------------------------------------

/**
 * Required members per contract, as data. `'*'` means "present, any type".
 * Lane A asserts these in selftest after every merge; that is how we find out
 * a lane's module drifted before it breaks someone else's lane.
 */
export const CONTRACT = {
  wind:     { sample: 'function', gustTelegraph: 'function' },
  world:    { anchors: 'object', heightAt: 'function', gardenBed: 'object', sunDir: 'object', solids: 'object', update: 'function' },
  sailRig:  { corners: 'object', attach: 'function', step: 'function', coverageOver: 'function', events: 'object', repair: 'function', trim: 'function', cornerPos: 'function', pondMass: 'function', pondCentroid: 'function', drainPondAt: 'function' },
  player:   { pos: 'object', carrying: '*', busy: '*', update: 'function' },
  interact: { register: 'function' },
  camera:   { object: 'object', yaw: 'number', update: 'function' },
  game:     { phase: 'string', on: 'function' },
  debris:   { pieces: 'object', step: 'function', spawn: 'function', setModels: 'function', clear: 'function' },
};

/**
 * The frozen DebrisPiece fields (SPRINT2 decision 5). Lane B's sail.step() reads
 * these off `debris.pieces` and applies impulses from them, so renaming one is a
 * breaking change to someone else's integrator, not a local tidy-up. Asserted
 * against live pieces in c.test.js — if this table and debris.js disagree, the
 * selftest says so before Lane B's cloth does.
 */
export const DEBRIS_PIECE_FIELDS = {
  x: 'number', y: 'number', z: 'number',
  vx: 'number', vy: 'number', vz: 'number',
  r: 'number', mass: 'number', model: 'string',
};

/**
 * @param {string} name  A key of CONTRACT.
 * @param {object} obj   The implementation to check.
 * @returns {string[]}   Human-readable problems; empty means it conforms.
 */
export function checkContract(name, obj) {
  const spec = CONTRACT[name];
  if (!spec) return [`unknown contract '${name}'`];
  if (!obj) return [`${name}: implementation is ${obj}`];
  const problems = [];
  for (const [key, want] of Object.entries(spec)) {
    const got = typeof obj[key];
    if (got === 'undefined') problems.push(`${name}.${key} missing`);
    else if (want !== '*' && got !== want) problems.push(`${name}.${key} is ${got}, want ${want}`);
  }
  return problems;
}

// ---------------------------------------------------------------------------
// Stub wind — so B, D and the HUD can run before Lane C lands weather.js
// ---------------------------------------------------------------------------

/**
 * A deterministic stand-in for weather.js that satisfies the Wind contract.
 * It ports the prototype's gust shape (telegraph 1.5s → ramp 0.8s → hold 1.7s
 * → fade 1.0s) and its base ramp, but is uniform in space and has no direction
 * change, no tree shadows and no noise. Lane C replaces it wholesale; nobody
 * should tune against it.
 *
 * @param {object} [opts]
 * @param {number} [opts.seed=1]
 * @param {number} [opts.stormLen=STORM_LEN]
 * @param {boolean} [opts.calm=false]  Calm-day breeze only — no storm ramp, no gusts.
 * @returns {Wind}
 */
export function createStubWind(opts = {}) {
  const { seed = 1, stormLen = STORM_LEN, calm = false } = opts;

  const TELEGRAPH = 1.5, RAMP = 0.8, HOLD = 1.7, FADE = 1.0;
  const CYCLE = TELEGRAPH + RAMP + HOLD + FADE;   // 5.0 s, matches prototype

  // Precompute the whole gust schedule up front: that keeps sample() pure in t
  // (no hidden state advancing per call) so selftest can sample out of order.
  const rand = rng(seed);
  const gusts = [];
  for (let t = 3; t < stormLen + CYCLE; ) {
    const p = Math.min(1, t / stormLen);
    gusts.push({ start: t, power: 12 + rand() * 16 + 10 * p });
    t += CYCLE + 5 + rand() * 7;
  }

  const dirAt = (t) => 0.9 + 0.25 * Math.sin(t * 0.13);

  /** Gust contribution in m/s at time t, plus the gust in flight. */
  function gustAt(t) {
    for (const g of gusts) {
      const gt = t - g.start;
      if (gt < 0 || gt >= CYCLE) continue;
      let mag;
      if (gt < TELEGRAPH) mag = 0;
      else if (gt < TELEGRAPH + RAMP) mag = g.power * (gt - TELEGRAPH) / RAMP;
      else if (gt < TELEGRAPH + RAMP + HOLD) mag = g.power;
      else mag = g.power * (CYCLE - gt) / FADE;
      return { mag, g, gt };
    }
    return { mag: 0, g: null, gt: 0 };
  }

  const out = new THREE.Vector3();

  return {
    sample(_pos, t) {
      if (calm) {
        const dir = dirAt(t);
        const s = 4 + 0.6 * Math.sin(t * 0.7);
        return out.set(Math.cos(dir) * s, 0, Math.sin(dir) * s);
      }
      const base = 8 + 26 * Math.min(1, (t / stormLen) * 1.6);
      const speed = base + gustAt(t).mag;
      const dir = dirAt(t);
      return out.set(Math.cos(dir) * speed, 0, Math.sin(dir) * speed);
    },

    gustTelegraph(t) {
      if (calm) return null;
      const { g, gt } = gustAt(t);
      if (!g || gt >= TELEGRAPH) return null;   // already landed: not a warning any more
      return { eta: TELEGRAPH - gt, dir: dirAt(t), power: g.power };
    },
  };
}
