import type { Stage } from '../scenes/stage';
import { Food, makeFood } from './food';
import {
  add, centroid, cvWord, len, norm, perp, pieceCV, pointInPoly, polyArea, polyDiameter,
  scale, segPolyCrossings, splitConvexByLine, sub, type V,
} from './geom';

export type CutQuality = 'clean' | 'ragged' | 'crime';

// cut thresholds, px/frame at 60fps (the calibration surface — tuned by harness)
const CUT = {
  clean: 11, // peak blade speed for a clean pass
  squash: 4.5, // below this a wet food splats instead of cutting
  minChordFrac: 0.45, // the pass must span this much of the food to count as a cut
  cleanChordFrac: 0.6, // and this much to be graded clean
  bladeTransfer: 0.42, // fraction of blade speed the pieces carry off (px/step)
  separate: 1.4, // px/step the halves spring apart (Matter velocity is per-step, not per-sec)
  gap: 4, // px each half is nudged off the cut line so they don't spawn overlapping
};

const worldVerts = (b: MatterJS.BodyType): V[] => (b.vertices ?? []).map((v) => ({ x: v.x, y: v.y }));

interface Track {
  entry: V;
  pathLen: number;
  frames: number;
  peakSpeed: number;
}

export interface CutResult {
  quality: CutQuality;
  pieces: number;
  cv: number;
  speed: number;
  chordFrac: number;
}

/**
 * The cut. A blade tip is driven across the board (by the harness now, by the
 * gripped hand at M4). When the tip passes THROUGH a food fast enough the food
 * splits along the tip's chord into two real bodies; a slow press on a wet food
 * squashes into a splat instead. Honest geometry — no canned "sliced" anim.
 */
export class Cutter {
  foods: Food[] = [];
  splats: { mass: number }[] = [];
  last: CutResult | null = null;
  private tip: V | null = null;
  private track = new Map<Food, Track>();

  constructor(private stage: Stage) {}

  add(food: Food): void {
    this.foods.push(food);
  }

  /** Start a fresh stroke from `p` (clears any in-progress pass). */
  resetBlade(p: V): void {
    this.tip = p;
    this.track.clear();
  }

  /** Advance the blade tip to `p` for one frame and run cut detection. */
  moveBlade(p: V): void {
    const prev = this.tip ?? p;
    this.tip = p;
    const stepLen = len(sub(p, prev));
    for (const food of [...this.foods]) {
      const verts = worldVerts(food.body);
      const inNow = pointInPoly(p, verts);
      const st = this.track.get(food);
      if (!st) {
        const crossings = segPolyCrossings(prev, p, verts);
        if (inNow || crossings.length > 0) {
          this.track.set(food, { entry: crossings[0] ?? prev, pathLen: stepLen, frames: 1, peakSpeed: stepLen });
        }
      } else {
        st.pathLen += stepLen;
        st.frames++;
        st.peakSpeed = Math.max(st.peakSpeed, stepLen);
        if (!inNow) {
          const crossings = segPolyCrossings(prev, p, verts);
          const exit = crossings.length ? crossings[crossings.length - 1] : prev;
          this.commit(food, st.entry, exit, st.peakSpeed);
          this.track.delete(food);
        }
      }
    }
  }

  private commit(food: Food, entry: V, exit: V, peakSpeed: number): void {
    const verts = worldVerts(food.body);
    const diam = polyDiameter(verts) || 1;
    const chordFrac = len(sub(exit, entry)) / diam;
    const mat = food.material;
    const power = peakSpeed * (1 - 0.5 * mat.resistance); // tougher skin, harder to cut

    // too slow or too shallow → not a clean split
    if (power < CUT.squash || chordFrac < CUT.minChordFrac) {
      if (mat.moisture > 0.4 && power < CUT.squash) {
        this.splat(food, peakSpeed);
      } else {
        this.last = { quality: 'crime', pieces: 1, cv: 0, speed: +peakSpeed.toFixed(2), chordFrac: +chordFrac.toFixed(2) };
      }
      return;
    }

    const parts = splitConvexByLine(verts, entry, exit);
    if (!parts) return;

    const quality: CutQuality = power >= CUT.clean && chordFrac >= CUT.cleanChordFrac ? 'clean' : 'ragged';
    this.stage.removeProp(food.body);
    this.foods = this.foods.filter((f) => f !== food);

    const dir = norm(sub(exit, entry));
    const n = perp(dir); // separation normal (splitConvexByLine returns [left, right] of dir)
    const bladeVel = scale(dir, peakSpeed * CUT.bladeTransfer); // peakSpeed is px/step already
    parts.forEach((pv, i) => {
      const c = centroid(pv);
      const side = i === 0 ? 1 : -1;
      // spawn each half nudged off the cut line so the two bodies never overlap
      // at birth — coincident bodies make Matter eject them across the county.
      const pos = add(c, scale(n, side * CUT.gap));
      const piece = makeFood(this.stage, pv, pos.x, pos.y, mat, food.color, food.face, food.kind + 'p');
      this.stage.matter.body.setVelocity(piece.body, add(bladeVel, scale(n, side * CUT.separate)));
      this.foods.push(piece);
    });

    const areas = parts.map(polyArea);
    this.last = {
      quality,
      pieces: 2,
      cv: +pieceCV(areas).toFixed(3),
      speed: +peakSpeed.toFixed(2),
      chordFrac: +chordFrac.toFixed(2),
    };
  }

  private splat(food: Food, peakSpeed: number): void {
    const verts = worldVerts(food.body);
    const c = centroid(verts);
    const mass = +((polyArea(verts) / 900) * food.material.moisture).toFixed(2);
    this.splats.push({ mass });
    this.stage.removeProp(food.body);
    this.foods = this.foods.filter((f) => f !== food);
    this.stage.splat(c.x, c.y, Math.sqrt(polyArea(verts) / Math.PI) * 1.3, food.color);
    this.last = { quality: 'crime', pieces: 0, cv: 0, speed: +peakSpeed.toFixed(2), chordFrac: 0 };
    // ponytail: slippery-floor patch deferred — no one carries over it until M4.
  }

  /** Everything the judge and the harness read off a cut. */
  stats() {
    const areas = this.foods.map((f) => polyArea(worldVerts(f.body)));
    const cv = pieceCV(areas);
    return {
      pieces: this.foods.length,
      cv: +cv.toFixed(3),
      cvWord: cvWord(cv),
      splatMass: +this.splats.reduce((a, b) => a + b.mass, 0).toFixed(2),
      last: this.last,
    };
  }
}
