import type { Stage } from '../scenes/stage';
import { box, Food, FOODS, makeFood } from './food';

/**
 * The sando. The stack is DETECTED, not declared: whatever food bodies are
 * physically resting in the plate zone, sorted bottom→top, IS the sando — so a
 * human dropping pieces by marionette hand and the harness placing them are
 * judged identically. Stability is honest Matter stacking; the judge scores
 * lean, COM drift, overhang off the real bodies.
 */

interface LayerDef {
  w: number;
  h: number;
  color: number;
  face: string;
}

const L = (kind: string, w: number, h: number): LayerDef => ({
  w, h, color: FOODS[kind].color, face: FOODS[kind].face,
});

export const LAYERS: Record<string, LayerDef> = {
  bread: L('bread', 94, 24),
  tomato: L('tomato', 78, 18),
  cheese: L('cheese', 82, 16),
  katsu: L('katsu', 88, 22),
  egg: L('egg', 70, 20),
  tofu: L('tofu', 74, 22),
  // the ura pantry: nori is comically thin, daikon comically tall — the layer
  // SHAPES are the difficulty, same physics as everything else.
  nori: L('nori', 96, 7),
  ebi: L('ebi', 72, 18),
  daikon: L('daikon', 46, 56),
  konnyaku: L('konnyaku', 62, 22),
};

export interface SandoStats {
  layers: number;
  leanDeg: number; // tilt of the stack axis from vertical
  comDrift: number; // px the worst layer sits off the plate centre
  overhang: number; // px the worst layer juts past the one below its support
  standing: boolean;
}

/** Materialise one slice-shaped layer food at (x, y). Caller registers it. */
export function spawnLayer(stage: Stage, kind: string, x: number, y: number): Food {
  const def = LAYERS[kind] ?? LAYERS.bread;
  const mat = FOODS[kind]?.material ?? FOODS.bread.material;
  return makeFood(stage, box(def.w, def.h), x, y, mat, def.color, def.face, kind);
}

const deg = (r: number): number => +((r * 180) / Math.PI).toFixed(1);
const halfW = (b: MatterJS.BodyType): number => (b.bounds.max.x - b.bounds.min.x) / 2;
const ZONE_HALF = 82; // px either side of plate centre that counts as "on the dish"
const IN_FLIGHT = 5; // px/step — anything faster is passing through, not stacked

export class Sando {
  constructor(private stage: Stage, public plateX = 980, public plateTopY = 634) {}

  /** The physical stack: foods resting in the plate zone, bottom→top. */
  get layers(): Food[] {
    return this.stage.cutter.foods
      .filter((f) => {
        const b = f.body;
        return (
          Math.abs(b.position.x - this.plateX) < ZONE_HALF &&
          b.position.y < this.plateTopY + 6 &&
          Math.hypot(b.velocity.x, b.velocity.y) < IN_FLIGHT
        );
      })
      .sort((a, b) => b.body.position.y - a.body.position.y);
  }

  /** Base kinds bottom→top ('tomatop' piece → 'tomato'). What the judge reads. */
  kinds(): string[] {
    return this.layers.map((f) => f.kind.replace(/p+$/, ''));
  }

  /** Top surface of the current stack (or the plate if empty). */
  private topY(): number {
    const ls = this.layers;
    if (!ls.length) return this.plateTopY;
    return Math.min(...ls.map((f) => f.body.bounds.min.y));
  }

  /** Harness helper: materialise one layer on top and settle. The spawned food
   *  registers with the cutter's food list, so detection sees it like any other. */
  addLayer(kind: string, offsetX = 0, settleFrames = 45): Food {
    const def = LAYERS[kind] ?? LAYERS.bread;
    const y = this.topY() - def.h / 2 - 1;
    const food = spawnLayer(this.stage, kind, this.plateX + offsetX, y);
    this.stage.cutter.add(food);
    this.stage.auto = false;
    for (let i = 0; i < settleFrames; i++) this.stage.stepSim();
    return food;
  }

  /** Remove everything currently in the plate zone. */
  clear(): void {
    for (const f of this.layers) {
      this.stage.removeProp(f.body);
      this.stage.cutter.foods = this.stage.cutter.foods.filter((x) => x !== f);
    }
  }

  stats(): SandoStats {
    const ls = this.layers;
    const n = ls.length;
    if (n === 0) return { layers: 0, leanDeg: 0, comDrift: 0, overhang: 0, standing: false };
    const bottom = ls[0].body, top = ls[n - 1].body;
    const leanDeg = deg(Math.atan2(top.position.x - bottom.position.x, Math.max(1, bottom.position.y - top.position.y)));
    let comDrift = 0, overhang = 0, stackH = 0;
    for (let i = 0; i < n; i++) {
      const b = ls[i].body;
      comDrift = Math.max(comDrift, Math.abs(b.position.x - this.plateX));
      stackH += b.bounds.max.y - b.bounds.min.y;
      if (i > 0) {
        const below = ls[i - 1].body;
        const off = Math.abs(b.position.x - below.position.x);
        overhang = Math.max(overhang, off - Math.max(0, halfW(below) - halfW(b)));
      }
    }
    // Standing = the tower still has its height: a collapse drops the top layer
    // (and usually scatters pieces out of the zone entirely — also honest).
    const elevation = this.plateTopY - top.position.y;
    const standing = elevation > 0.45 * stackH;
    return { layers: n, leanDeg, comDrift: +comDrift.toFixed(1), overhang: +overhang.toFixed(1), standing };
  }

  /** The earthquake: shove every stacked layer sideways (px/step). */
  nudge(force: number): void {
    const b = this.stage.matter.body;
    for (const f of this.layers) {
      b.setVelocity(f.body, { x: f.body.velocity.x + force, y: f.body.velocity.y });
    }
  }
}
