import * as THREE from 'three';
import { makeCutleryMesh, type Tool } from '../sim/cutlery';
import { loadProp } from './assets';
import type { SpreadDef } from '../sim/spreads';

/**
 * The knife you actually hold. Two nested groups so the tilt is about the blade,
 * not about the mouse cursor: `yaw` sets how the knife is held, `tilt` is the
 * one control the whole mechanic hangs off.
 */
export class KnifeRig {
  readonly root = new THREE.Group();
  private yaw = new THREE.Group();
  private tilt = new THREE.Group();
  private mesh: THREE.Group;
  private blob: THREE.Mesh;
  private blobMat: THREE.MeshStandardMaterial;
  private bladeCentre: number;

  constructor(tool: Tool) {
    this.bladeCentre = 0.12 + tool.headL * 0.5;
    this.mesh = makeCutleryMesh(tool);
    // Slide the piece so the middle of its head sits at the rig's origin — that
    // point is the contact patch, and it's what we rotate about.
    this.mesh.position.z = -this.bladeCentre;
    this.tilt.add(this.mesh);
    this.yaw.add(this.tilt);
    this.root.add(this.yaw);
    // Held like a right-handed person spreading: handle down toward the player.
    this.yaw.rotation.y = -0.62;

    // The load on the blade, so you can see when you've run out.
    const blobGeo = new THREE.SphereGeometry(1, 14, 10);
    blobGeo.scale(tool.headW * 0.42, 0.035, tool.headL * 0.34);
    this.blobMat = new THREE.MeshStandardMaterial({ color: 0xf0c040, roughness: 0.32 });
    this.blob = new THREE.Mesh(blobGeo, this.blobMat);
    this.blob.position.set(0, 0.028, 0);
    this.tilt.add(this.blob);
  }

  setTool(tool: Tool): void {
    this.tilt.remove(this.mesh);
    this.mesh = makeCutleryMesh(tool);
    this.bladeCentre = 0.12 + tool.headL * 0.5;
    this.mesh.position.z = -this.bladeCentre;
    this.tilt.add(this.mesh);
  }

  setSpread(def: SpreadDef | null): void {
    if (def) this.blobMat.color.setRGB(def.color[0], def.color[1], def.color[2]);
  }

  /** angle 0..1; load 0..1 */
  update(pos: THREE.Vector3, angle: number, load: number, topY: number): void {
    // Tilt lifts the handle and drops the blade onto its edge. At angle 1 the
    // knife is all but vertical, which is exactly how you scrape burnt toast.
    const tiltRad = angle * 1.38;
    this.tilt.rotation.x = -tiltRad;
    // Ride the contact patch just above the toast whatever the tilt is doing.
    const lift = Math.sin(tiltRad) * 0.045 + 0.012;
    this.root.position.set(pos.x, Math.max(topY, pos.y) + lift, pos.z);
    this.blob.visible = load > 0.01;
    const s = 0.35 + Math.min(1, load / 0.6) * 0.75;
    this.blob.scale.set(s, Math.min(1.6, s * (0.6 + load)), s);
  }
}

/**
 * The pot you dip into — one per order, because you only ever get one spread.
 *
 * The vessel is a generated GLB; the thing you actually dip into is an invisible
 * box at its mouth. Raycasting the generated mesh directly would be at the mercy
 * of whatever the mesher produced, and the dip target needs to be exactly where
 * the player expects it.
 */
export function makeSpreadPot(def: SpreadDef): { root: THREE.Group; hit: THREE.Mesh } {
  const g = new THREE.Group();
  const isButter = def.id === 'butter';

  // material.visible = false, not object.visible = false: the renderer skips it
  // either way, but the raycaster still needs a live object to hit.
  const hit = new THREE.Mesh(
    new THREE.BoxGeometry(isButter ? 0.8 : 0.62, 0.08, isButter ? 0.5 : 0.62),
    new THREE.MeshBasicMaterial({ visible: false }),
  );
  hit.position.y = isButter ? 0.34 : 0.56;
  g.add(hit);

  const file =
    isButter ? 'butter_dish'
    : def.id === 'peanut' ? 'pb_jar'
    : def.id === 'marmalade' ? 'marmalade_jar'
    : 'mitey_jar';
  loadProp(`/assets/models/${file}.glb`, isButter ? 1.5 : 1.15)
    .then((prop) => g.add(prop))
    .catch(() => {
      // Asset still generating (or missing): a plain glass jar of the right
      // colour keeps the order playable.
      const jar = new THREE.Mesh(
        new THREE.CylinderGeometry(0.42, 0.4, 0.66, 24),
        new THREE.MeshStandardMaterial({
          color: 0xdfe6e6,
          roughness: 0.12,
          transparent: true,
          opacity: 0.42,
        }),
      );
      jar.position.y = 0.33;
      const fill = new THREE.Mesh(
        new THREE.CylinderGeometry(0.36, 0.34, 0.5, 24),
        new THREE.MeshStandardMaterial({
          color: new THREE.Color(def.color[0], def.color[1], def.color[2]),
          roughness: 0.35,
        }),
      );
      fill.position.y = 0.29;
      g.add(jar, fill);
    });

  return { root: g, hit };
}
