import type { Stage } from '../scenes/stage';

/**
 * The marionette. A control bar (the player's proxy) hangs 5 strings — head,
 * two hands, two knees — over a 9-segment silhouette puppet. The bar snaps to
 * the mouse instantly; the LAG the whole game is built on comes from soft
 * string constraints (stiffness < 1), not from any input buffer. The head
 * string bears the posture; hands and knees are floppy on purpose.
 *
 * Bodies live in the Stage's Matter world; the sim owns the control + readouts.
 */

// --- tuning knobs (the calibration surface — tune by harness, not by guess) ---
const K = {
  // string stiffness / damping. head + back hold posture; limbs dangle.
  headK: 0.14, headD: 0.09,
  backK: 0.11, backD: 0.08,
  handK: 0.045, handD: 0.06,
  kneeK: 0.07, kneeD: 0.06,
  jointK: 0.8, // near-rigid pin joints between segments
  gripK: 0.75, // clamp stiffness when a hand grabs
  gripReach: 60, // px a hand can grab across
  barHomeY: 175,
  reachDrop: 130, // extra bar lowering while LMB held
  group: -7, // shared negative collision group: puppet never collides with itself
  railMin: 30, railMax: 1250, // bar x clamp
  barCeil: 40, barFloor: 380, // bar y clamp (before reach)
  tiltGain: 0.012, tiltMax: 0.5, // horizontal bar speed -> lean radians
};

type Seg =
  | 'head' | 'chest' | 'hips'
  | 'uArmL' | 'lArmL' | 'uArmR' | 'lArmR'
  | 'legL' | 'legR';

/** Unlockable strings — difficulty modifiers, not new mechanics. Multipliers on
 *  string stiffness/damping only; the puppet itself never changes. */
export type StringStyle = 'hemp' | 'rubber' | 'chain' | 'silk';
export const STRING_STYLES: Record<StringStyle, { k: number; d: number; blurb: string }> = {
  hemp: { k: 1, d: 1, blurb: 'the honest string' },
  rubber: { k: 0.45, d: 0.5, blurb: 'bouncy. +lag +comedy' },
  chain: { k: 1.7, d: 2.4, blurb: 'heavy. slow. precise' },
  silk: { k: 1.35, d: 1.15, blurb: 'tighter, for cowards' },
};

type Vec = { x: number; y: number };

export interface Pose {
  head: number; chest: number; hips: number;
  armL: number; armR: number; legL: number; legR: number;
  lean: number; // chest tilt from vertical, degrees
  upright: boolean;
}

export class Marionette {
  readonly seg = {} as Record<Seg, MatterJS.BodyType>;
  private strings = {} as Record<'head' | 'back' | 'handL' | 'handR' | 'kneeL' | 'kneeR', MatterJS.ConstraintType>;
  private gripCon: (MatterJS.ConstraintType | null)[] = [null, null]; // [L, R]
  private gripped = false;
  cut = false; // strings severed → ragdoll

  barX: number;
  barY = K.barHomeY;
  tilt = 0;
  readonly barHomeY = K.barHomeY;
  private prevBarX: number;
  private home0 = {} as Record<Seg, { x: number; y: number }>;

  /** Bodies the hands can grab (crate, later food). Stage fills this. */
  gripTargets: MatterJS.BodyType[] = [];

  constructor(private stage: Stage, homeX = 400, public style: StringStyle = 'hemp') {
    this.barX = homeX;
    this.prevBarX = homeX;
    const m = stage.matter;
    const g = { collisionFilter: { group: K.group } };

    const rect = (x: number, y: number, w: number, h: number) =>
      m.add.rectangle(x, y, w, h, { ...g, frictionAir: 0.05, friction: 0.7 });
    const view = (b: MatterJS.BodyType, w: number, h: number) => stage.addBody(b, w, h, 0x111111);

    // 9 segments laid out standing, feet planted on the floor (~642) so the rig
    // has traction and self-rights against the strings instead of pure-pendulum.
    const hx = homeX;
    this.seg.head = m.add.circle(hx, 388, 20, { ...g, frictionAir: 0.05 });
    stage.addCircle(this.seg.head, 20, 0x111111);
    this.seg.chest = rect(hx, 437, 44, 46); view(this.seg.chest, 44, 46);
    this.seg.hips = rect(hx, 482, 44, 40); view(this.seg.hips, 44, 40);
    this.seg.uArmL = rect(hx - 30, 439, 13, 46); view(this.seg.uArmL, 13, 46);
    this.seg.lArmL = rect(hx - 30, 486, 11, 46); view(this.seg.lArmL, 11, 46);
    this.seg.uArmR = rect(hx + 30, 439, 13, 46); view(this.seg.uArmR, 13, 46);
    this.seg.lArmR = rect(hx + 30, 486, 11, 46); view(this.seg.lArmR, 11, 46);
    this.seg.legL = rect(hx - 12, 587, 16, 110); view(this.seg.legL, 16, 110);
    this.seg.legR = rect(hx + 12, 587, 16, 110); view(this.seg.legR, 16, 110);

    // pin joints (length 0 = hinge around shared point)
    const pin = (a: MatterJS.BodyType, pa: Vec, b: MatterJS.BodyType, pb: Vec) =>
      m.add.constraint(a, b, 0, K.jointK, { pointA: pa, pointB: pb, damping: 0.1 });
    pin(this.seg.chest, { x: 0, y: -23 }, this.seg.head, { x: 0, y: 20 }); // neck
    pin(this.seg.hips, { x: 0, y: -20 }, this.seg.chest, { x: 0, y: 23 }); // spine
    pin(this.seg.chest, { x: -22, y: -16 }, this.seg.uArmL, { x: 0, y: -23 });
    pin(this.seg.uArmL, { x: 0, y: 23 }, this.seg.lArmL, { x: 0, y: -23 });
    pin(this.seg.chest, { x: 22, y: -16 }, this.seg.uArmR, { x: 0, y: -23 });
    pin(this.seg.uArmR, { x: 0, y: 23 }, this.seg.lArmR, { x: 0, y: -23 });
    pin(this.seg.hips, { x: -14, y: 20 }, this.seg.legL, { x: 0, y: -55 });
    pin(this.seg.hips, { x: 14, y: 20 }, this.seg.legR, { x: 0, y: -55 });

    this.buildStrings();

    for (const key of Object.keys(this.seg) as Seg[]) {
      this.home0[key] = { x: this.seg[key].position.x, y: this.seg[key].position.y };
    }
    this.setBar(this.barX, K.barHomeY, 0);
  }

  /** (Re)create the 6 control strings from the bar to the puppet. Called at
   *  build and again after a string cut when the puppet is restrung. */
  private buildStrings(): void {
    const m = this.stage.matter;
    const s = STRING_STYLES[this.style];
    const string = (b: MatterJS.BodyType, pb: Vec, length: number, k: number, d: number) =>
      m.add.worldConstraint(b, length, Math.min(0.95, k * s.k), {
        pointA: { x: this.barX, y: K.barHomeY },
        pointB: pb, damping: d * s.d, render: { visible: false },
      });
    // rest lengths match the standing pose so the strings hold posture without
    // hauling the feet off the floor. head + back together fix torso rotation.
    this.strings.head = string(this.seg.head, { x: 0, y: -20 }, 195, K.headK, K.headD);
    this.strings.back = string(this.seg.chest, { x: 0, y: -23 }, 240, K.backK, K.backD);
    this.strings.handL = string(this.seg.lArmL, { x: 0, y: 23 }, 335, K.handK, K.handD);
    this.strings.handR = string(this.seg.lArmR, { x: 0, y: 23 }, 335, K.handK, K.handD);
    this.strings.kneeL = string(this.seg.legL, { x: 0, y: -15 }, 395, K.kneeK, K.kneeD);
    this.strings.kneeR = string(this.seg.legR, { x: 0, y: -15 }, 395, K.kneeK, K.kneeD);
    this.cut = false;
  }

  /** SHUBATTO. Every control string deleted in one frame — the puppet becomes a
   *  ragdoll (pin joints survive, so it stays connected as it collapses). Drops
   *  anything held. The samurai's fail-state; the whole game points at this. */
  cutStrings(): void {
    if (this.cut) return;
    this.setGrip(false);
    for (const key of Object.keys(this.strings) as (keyof Marionette['strings'])[]) {
      this.stage.matter.world.remove(this.strings[key]);
    }
    this.strings = {} as Marionette['strings'];
    this.cut = true;
  }

  /** Teleport every segment back to its start pose and drop anything held.
   *  Harness-only — lets tests run back to back from a known state. Restrings
   *  the puppet if a string cut had happened. */
  reset(homeX = 400): void {
    this.setGrip(false);
    if (this.cut) this.buildStrings();
    const b = this.stage.matter.body;
    const dx = homeX - 400;
    for (const key of Object.keys(this.seg) as Seg[]) {
      b.setPosition(this.seg[key], { x: this.home0[key].x + dx, y: this.home0[key].y }, false);
      b.setAngle(this.seg[key], 0, false);
      b.setVelocity(this.seg[key], { x: 0, y: 0 });
      b.setAngularVelocity(this.seg[key], 0);
    }
    this.prevBarX = homeX;
    this.setBar(homeX, K.barHomeY, 0);
  }

  /** Anchor offsets from the bar centre, before tilt rotation. */
  private static ANCHOR: Record<keyof Marionette['strings'], Vec> = {
    head: { x: 0, y: -8 }, back: { x: 0, y: 8 },
    handL: { x: -46, y: 12 }, handR: { x: 46, y: 12 },
    kneeL: { x: -22, y: 26 }, kneeR: { x: 22, y: 26 },
  };

  /** Move the control bar. tilt (radians) rotates the whole string array — a
   *  fast horizontal move tilts the bar and the puppet leans into it. */
  setBar(x: number, y: number, tilt: number): void {
    this.barX = x;
    this.barY = y;
    this.tilt = tilt;
    const c = Math.cos(tilt), s = Math.sin(tilt);
    for (const key of Object.keys(this.strings) as (keyof Marionette['strings'])[]) {
      const o = Marionette.ANCHOR[key];
      const p = this.strings[key].pointA as Vec;
      p.x = x + o.x * c - o.y * s;
      p.y = y + o.x * s + o.y * c;
    }
  }

  /** Raw pointer/harness input → bar move + grip. Tilt comes from how fast the
   *  bar is travelling sideways; LMB (reach) drops the bar to grab low. */
  applyInput(mouseX: number, mouseY: number, reach: boolean, grip: boolean): void {
    if (this.cut) return; // no strings to pull
    const clamp = (v: number, a: number, b: number) => (v < a ? a : v > b ? b : v);
    const x = clamp(mouseX, K.railMin, K.railMax);
    const y = clamp(mouseY, K.barCeil, K.barFloor) + (reach ? K.reachDrop : 0);
    const tilt = clamp((x - this.prevBarX) * K.tiltGain, -K.tiltMax, K.tiltMax);
    this.prevBarX = x;
    this.setBar(x, y, tilt);
    this.setGrip(grip);
  }

  /** hand tip world positions. */
  handWorld(): { L: Vec; R: Vec } {
    return { L: this.handTipWorld(this.seg.lArmL), R: this.handTipWorld(this.seg.lArmR) };
  }

  /** The 6 strings as bar-anchor → body-attach world segments, for drawing.
   *  Empty once the strings are cut. */
  stringSegments(): { ax: number; ay: number; bx: number; by: number }[] {
    if (this.cut) return [];
    const out: { ax: number; ay: number; bx: number; by: number }[] = [];
    for (const key of Object.keys(this.strings) as (keyof Marionette['strings'])[]) {
      const c = this.strings[key] as unknown as { pointA: Vec; pointB: Vec; bodyB: MatterJS.BodyType };
      const a = c.pointA, pb = c.pointB, body = c.bodyB;
      const co = Math.cos(body.angle), si = Math.sin(body.angle);
      out.push({ ax: a.x, ay: a.y, bx: body.position.x + pb.x * co - pb.y * si, by: body.position.y + pb.x * si + pb.y * co });
    }
    return out;
  }

  /** The wooden control bar as a segment (centre ± half-width, tilted). */
  barSegment(half = 58): { x1: number; y1: number; x2: number; y2: number } {
    const c = Math.cos(this.tilt), s = Math.sin(this.tilt);
    return { x1: this.barX - half * c, y1: this.barY - half * s, x2: this.barX + half * c, y2: this.barY + half * s };
  }

  get gripping(): boolean {
    return this.gripped;
  }
  get holding(): boolean {
    return this.gripCon.some((c) => c !== null);
  }

  /** The bodies currently clamped in the hands (grab constraints are arm→target,
   *  so the target is bodyB). */
  held(): MatterJS.BodyType[] {
    const out: MatterJS.BodyType[] = [];
    for (const c of this.gripCon) {
      const t = (c as unknown as { bodyB?: MatterJS.BodyType } | null)?.bodyB;
      if (t) out.push(t);
    }
    return out;
  }

  /** Both hands want to clamp. Turning grip OFF drops whatever's held. The
   *  actual grab happens in update(): while gripped, any empty hand latches
   *  onto the nearest target the instant it comes within reach — so you can
   *  lower a closed hand onto food and it catches on contact. */
  setGrip(on: boolean): void {
    if (on === this.gripped) return;
    this.gripped = on;
    if (!on) {
      for (const c of this.gripCon) if (c) this.stage.matter.world.remove(c);
      this.gripCon = [null, null];
    }
  }

  /** Per-frame: latch empty gripping hands onto anything in reach. */
  update(): void {
    if (!this.gripped) return;
    const arms = [this.seg.lArmL, this.seg.lArmR];
    for (let i = 0; i < 2; i++) {
      if (this.gripCon[i]) continue;
      const tipW = this.handTipWorld(arms[i]);
      const t = this.nearestTarget(tipW);
      if (!t) continue;
      this.gripCon[i] = this.stage.matter.add.constraint(arms[i], t, 0, K.gripK, {
        pointA: { x: 0, y: 23 }, pointB: this.toLocal(t, tipW), damping: 0.1,
      });
    }
  }

  private handTipWorld(arm: MatterJS.BodyType): Vec {
    const a = arm.angle;
    return { x: arm.position.x - Math.sin(a) * 23, y: arm.position.y + Math.cos(a) * 23 };
  }
  private nearestTarget(p: Vec): MatterJS.BodyType | null {
    let best: MatterJS.BodyType | null = null, bd = K.gripReach;
    for (const t of this.gripTargets) {
      const d = Math.hypot(t.position.x - p.x, t.position.y - p.y);
      if (d < bd) { bd = d; best = t; }
    }
    return best;
  }
  private toLocal(body: MatterJS.BodyType, world: Vec): Vec {
    const dx = world.x - body.position.x, dy = world.y - body.position.y;
    const c = Math.cos(-body.angle), s = Math.sin(-body.angle);
    return { x: dx * c - dy * s, y: dx * s + dy * c };
  }

  private deg(rad: number): number {
    return +((rad * 180) / Math.PI).toFixed(1);
  }

  pose(): Pose {
    const lean = this.deg(this.seg.chest.angle);
    const upright = this.seg.head.position.y < this.seg.hips.position.y && Math.abs(lean) < 45;
    return {
      head: this.deg(this.seg.head.angle),
      chest: this.deg(this.seg.chest.angle),
      hips: this.deg(this.seg.hips.angle),
      armL: this.deg(this.seg.lArmL.angle),
      armR: this.deg(this.seg.lArmR.angle),
      legL: this.deg(this.seg.legL.angle),
      legR: this.deg(this.seg.legR.angle),
      lean,
      upright,
    };
  }

  /** hips x — the thing that moves when the puppet walks. */
  get torsoX(): number {
    return this.seg.hips.position.x;
  }

  /** peak speed of any segment — 0 ≈ fully settled. */
  restSpeed(): number {
    let v = 0;
    for (const key of Object.keys(this.seg) as Seg[]) {
      v = Math.max(v, Math.hypot(this.seg[key].velocity.x, this.seg[key].velocity.y));
    }
    return v;
  }
}
