import * as THREE from 'three';

/**
 * The cutlery cast. Deliberately procedural rather than generated: these
 * silhouettes are gameplay — the drawer asks you to find "the dessert fork"
 * among things that are almost dessert forks, and that's only fair if the
 * differences are authored. It also keeps the tines thin without a mesher
 * mangling them, and lets each piece carry its own physics colliders.
 */

export type ToolId =
  | 'butter_knife'
  | 'dinner_knife'
  | 'steak_knife'
  | 'spreader'
  | 'dinner_fork'
  | 'dessert_fork'
  | 'teaspoon'
  | 'dessert_spoon'
  | 'soup_spoon';

export type ToolKind = 'knife' | 'fork' | 'spoon';

export interface Tool {
  id: ToolId;
  name: string;
  kind: ToolKind;
  /** Overall length in slice-units (1 unit ~ 11cm). */
  length: number;
  /** Width of the business end. */
  headW: number;
  headL: number;
  /** Multiplies the knife's contact patch — a spreader is wide, a steak knife isn't. */
  contactScale: number;
  /** How well it moves spread at all. */
  transferScale: number;
  /** Multiplier on gouge risk. Serrated things are bad news. */
  gougeProne: number;
  /** 0..1 — how blotchy the deposit is. A spoon can't lay a flat film. */
  blotch: number;
  /** Multiplier on tearing when the spread won't yield. */
  tearProne: number;
  /** The right tool for spreading. */
  ideal: boolean;
  blurb: string;
}

export const TOOLS: Record<ToolId, Tool> = {
  butter_knife: {
    id: 'butter_knife',
    name: 'Butter Knife',
    kind: 'knife',
    length: 1.75,
    headW: 0.2,
    headL: 0.72,
    contactScale: 1,
    transferScale: 1,
    gougeProne: 1,
    blotch: 0,
    tearProne: 1,
    ideal: true,
    blurb: 'Round-tipped, wide, dull. The correct answer.',
  },
  dinner_knife: {
    id: 'dinner_knife',
    name: 'Dinner Knife',
    kind: 'knife',
    length: 2.0,
    headW: 0.16,
    headL: 0.85,
    contactScale: 0.86,
    transferScale: 0.95,
    gougeProne: 1.35,
    blotch: 0.05,
    tearProne: 1.1,
    ideal: false,
    blurb: 'Longer, narrower, and it has opinions about the crumb.',
  },
  steak_knife: {
    id: 'steak_knife',
    name: 'Steak Knife',
    kind: 'knife',
    length: 1.95,
    headW: 0.13,
    headL: 0.88,
    contactScale: 0.62,
    transferScale: 0.8,
    gougeProne: 3.2,
    blotch: 0.12,
    tearProne: 1.6,
    ideal: false,
    blurb: 'Serrated. Every stroke is a small act of violence.',
  },
  spreader: {
    id: 'spreader',
    name: 'Pâté Spreader',
    kind: 'knife',
    length: 1.4,
    headW: 0.3,
    headL: 0.5,
    contactScale: 1.35,
    transferScale: 1.15,
    gougeProne: 0.55,
    blotch: 0,
    tearProne: 0.7,
    ideal: true,
    blurb: 'Stubby, wide, blameless. Somehow always at the back.',
  },
  dinner_fork: {
    id: 'dinner_fork',
    name: 'Dinner Fork',
    kind: 'fork',
    length: 1.85,
    headW: 0.26,
    headL: 0.42,
    contactScale: 0.7,
    transferScale: 0.55,
    gougeProne: 2.4,
    blotch: 0.75,
    tearProne: 3.0,
    ideal: false,
    blurb: 'Four tines. Four furrows.',
  },
  dessert_fork: {
    id: 'dessert_fork',
    name: 'Dessert Fork',
    kind: 'fork',
    length: 1.5,
    headW: 0.23,
    headL: 0.34,
    contactScale: 0.6,
    transferScale: 0.5,
    gougeProne: 2.2,
    blotch: 0.78,
    tearProne: 2.8,
    ideal: false,
    blurb: 'Like a dinner fork, but smaller. That is the entire difference.',
  },
  teaspoon: {
    id: 'teaspoon',
    name: 'Teaspoon',
    kind: 'spoon',
    length: 1.3,
    headW: 0.24,
    headL: 0.34,
    contactScale: 0.75,
    transferScale: 0.7,
    gougeProne: 0.5,
    blotch: 0.6,
    tearProne: 1.2,
    ideal: false,
    blurb: 'You can, technically. It will show.',
  },
  dessert_spoon: {
    id: 'dessert_spoon',
    name: 'Dessert Spoon',
    kind: 'spoon',
    length: 1.65,
    headW: 0.3,
    headL: 0.44,
    contactScale: 0.85,
    transferScale: 0.75,
    gougeProne: 0.45,
    blotch: 0.55,
    tearProne: 1.15,
    ideal: false,
    blurb: 'A teaspoon that has been to the gym.',
  },
  soup_spoon: {
    id: 'soup_spoon',
    name: 'Soup Spoon',
    kind: 'spoon',
    length: 1.6,
    headW: 0.38,
    headL: 0.4,
    contactScale: 0.9,
    transferScale: 0.7,
    gougeProne: 0.4,
    blotch: 0.62,
    tearProne: 1.1,
    ideal: false,
    blurb: 'Round. Deep. Utterly wrong, but confidently so.',
  },
};

export const TOOL_IDS = Object.keys(TOOLS) as ToolId[];

const STEEL = new THREE.MeshStandardMaterial({
  color: 0xd2d7dd,
  roughness: 0.24,
  metalness: 0.95,
});

/**
 * Extrude a profile drawn in shape-space (x = width, y = length) into a part
 * lying in the XZ plane: shape +y becomes +z, and the extrusion thickness ends
 * up centred on y=0.
 *
 * Done at the geometry level on purpose. Setting mesh.rotation.x = -PI/2 instead
 * sends a profile drawn toward +y to -z and one drawn toward -y to +z — which
 * silently lays the handle and the blade out in opposite directions, on top of
 * each other, and the piece is nowhere near where the code says it is.
 */
function extrudeFlat(shape: THREE.Shape, depth: number, bevel: number): THREE.BufferGeometry {
  const geo = new THREE.ExtrudeGeometry(shape, {
    depth,
    bevelEnabled: bevel > 0,
    bevelThickness: bevel,
    bevelSize: bevel,
    bevelSegments: 2,
    curveSegments: 12,
  });
  geo.rotateX(Math.PI / 2); // shape +y -> +z, extrusion depth -> -y
  geo.translate(0, depth / 2, 0);
  return geo;
}

/**
 * Build a piece of cutlery lying in the XZ plane, handle at -Z, head at +Z.
 * Y is thickness.
 */
export function makeCutleryMesh(tool: Tool): THREE.Group {
  const g = new THREE.Group();
  const L = tool.length;
  const handleL = L - tool.headL - 0.12;

  // handle: a tapered, slightly domed bar
  const handleShape = new THREE.Shape();
  const hw0 = 0.052; // at the neck
  const hw1 = 0.085; // at the butt
  handleShape.moveTo(-hw0, 0);
  handleShape.lineTo(-hw1 * 0.92, -handleL * 0.55);
  handleShape.quadraticCurveTo(-hw1, -handleL, 0, -handleL);
  handleShape.quadraticCurveTo(hw1, -handleL, hw1 * 0.92, -handleL * 0.55);
  handleShape.lineTo(hw0, 0);
  handleShape.closePath();
  const handle = new THREE.Mesh(extrudeFlat(handleShape, 0.036, 0.014), STEEL);
  handle.position.z = -0.02;
  g.add(handle);

  // neck
  const neck = new THREE.Mesh(new THREE.BoxGeometry(0.055, 0.028, 0.16), STEEL);
  neck.position.z = 0.06;
  g.add(neck);

  if (tool.kind === 'knife') g.add(makeBlade(tool));
  else if (tool.kind === 'fork') g.add(makeForkHead(tool));
  else g.add(makeSpoonBowl(tool));

  for (const c of g.children) {
    c.castShadow = true;
    c.receiveShadow = true;
  }
  return g;
}

function makeBlade(tool: Tool): THREE.Mesh {
  const w = tool.headW / 2;
  const l = tool.headL;
  const s = new THREE.Shape();
  s.moveTo(-0.028, 0);
  s.lineTo(-w * 0.8, l * 0.22);
  s.lineTo(-w, l * 0.55);
  // rounded tip for a butter knife, a point for the aggressive ones
  if (tool.id === 'butter_knife' || tool.id === 'spreader') {
    s.quadraticCurveTo(-w, l, 0, l);
    s.quadraticCurveTo(w, l, w, l * 0.55);
  } else {
    s.lineTo(-w * 0.55, l * 0.93);
    s.quadraticCurveTo(0, l * 1.02, w * 0.72, l * 0.86);
    s.lineTo(w, l * 0.55);
  }
  s.lineTo(w * 0.8, l * 0.22);
  s.lineTo(0.028, 0);
  s.closePath();

  const blade = new THREE.Mesh(extrudeFlat(s, 0.014, 0.006), STEEL);
  blade.position.set(0, 0, 0.12);
  return blade;
}

function makeForkHead(tool: Tool): THREE.Group {
  const g = new THREE.Group();
  const w = tool.headW / 2;
  const l = tool.headL;
  // the shoulder the tines grow out of
  const base = new THREE.Shape();
  base.moveTo(-0.03, 0);
  base.lineTo(-w, l * 0.34);
  base.lineTo(w, l * 0.34);
  base.lineTo(0.03, 0);
  base.closePath();
  const shoulder = new THREE.Mesh(extrudeFlat(base, 0.016, 0), STEEL);
  shoulder.position.set(0, 0, 0.12);
  g.add(shoulder);

  // four tines
  const tineL = l * 0.66;
  const tineW = (w * 2) / 7;
  for (let i = 0; i < 4; i++) {
    const x = (i - 1.5) * (w * 2) / 4;
    const tine = new THREE.Mesh(new THREE.BoxGeometry(tineW, 0.014, tineL), STEEL);
    tine.position.set(x, 0, 0.12 + l * 0.34 + tineL / 2);
    g.add(tine);
    const tip = new THREE.Mesh(new THREE.ConeGeometry(tineW * 0.5, 0.05, 6), STEEL);
    tip.rotation.x = Math.PI / 2;
    tip.position.set(x, 0, 0.12 + l * 0.34 + tineL + 0.02);
    g.add(tip);
  }
  return g;
}

function makeSpoonBowl(tool: Tool): THREE.Mesh {
  const geo = new THREE.SphereGeometry(0.5, 20, 14, 0, Math.PI * 2, 0, Math.PI * 0.52);
  geo.scale(tool.headW * 0.5, 0.11, tool.headL * 0.6);
  geo.rotateX(Math.PI); // open side up
  const bowl = new THREE.Mesh(geo, STEEL);
  bowl.position.set(0, 0.005, 0.12 + tool.headL * 0.42);
  return bowl;
}

/**
 * Compound collider primitives for the drawer, in the mesh's local space.
 * Boxes only, and few of them: a trimesh of a fork is both slow and a stability
 * nightmare when a dozen of them are tangled together.
 */
export interface ColliderPart {
  half: [number, number, number];
  pos: [number, number, number];
}

export function colliderParts(tool: Tool): ColliderPart[] {
  const L = tool.length;
  const handleL = L - tool.headL - 0.12;
  const parts: ColliderPart[] = [
    { half: [0.075, 0.03, handleL / 2], pos: [0, 0, -0.02 - handleL / 2] },
    { half: [0.03, 0.016, 0.08], pos: [0, 0, 0.06] },
  ];
  if (tool.kind === 'spoon') {
    parts.push({
      half: [tool.headW * 0.5, 0.055, tool.headL * 0.32],
      pos: [0, 0, 0.12 + tool.headL * 0.42],
    });
  } else {
    // one slab for a blade; for a fork this is the tine envelope, which is what
    // actually matters — individual tines catching each other is a physics trap.
    parts.push({
      half: [tool.headW * 0.5, 0.012, tool.headL * 0.5],
      pos: [0, 0, 0.12 + tool.headL * 0.5],
    });
  }
  return parts;
}
