import * as THREE from 'three';
import type { App, View } from '../core/app';
import { Rng } from '../core/rng';
import { Slice } from '../sim/slice';
import { BREADS, type BreadId } from '../sim/bread';
import { buildHeatMap, readToast, smellCue, toastStep } from '../sim/toasting';
import {
  SPREADS,
  SCRAPE_ANGLE,
  effectiveYield,
  pressureFromAngle,
  type SpreadDef,
  type SpreadId,
} from '../sim/spreads';
import { dip, newKnife, stroke, type Knife, type StrokeFx } from '../sim/spreading';
import { TOOLS, type ToolId } from '../sim/cutlery';
import { makeBench, makePlate, makeToaster, type Toaster } from './props';
import { KnifeRig, makeSpreadPot } from './spreadrig';
import { el, Panel } from '../ui/hud';
import { audio } from '../core/audio';
import type { Order } from '../game/orders';

const TOASTER_X = -1.5;
const PLATE_X = 1.45;
const PLATE_Z = 0.55;
const PLATE_Y = 0.05;
const POT_X = 3.15;
const POT_Z = 0.45;

export type Phase = 'ready' | 'toasting' | 'flying' | 'landed' | 'spreading';

interface CamShot {
  pos: THREE.Vector3;
  look: THREE.Vector3;
}

const SHOTS: Record<string, CamShot> = {
  toast: { pos: new THREE.Vector3(-1.1, 3.0, 4.9), look: new THREE.Vector3(-1.3, 0.85, -0.2) },
  spread: { pos: new THREE.Vector3(2.05, 2.4, 3.0), look: new THREE.Vector3(2.05, 0.1, 0.35) },
};

/** The bench: toaster, plate, pot, knife. Runs the toasting and spreading phases. */
export class KitchenView implements View {
  readonly root = new THREE.Group();
  private toaster: Toaster;
  private slice!: Slice;
  private heat!: Float32Array;
  private rng: Rng;

  phase: Phase = 'ready';
  power = 6;
  /** 0 = straight from the fridge, 1 = left out on the bench all morning. */
  butterSoftness = 0.15;
  toolId: ToolId = 'butter_knife';
  spreadId: SpreadId = 'butter';

  private leverT = 0;
  private vel = new THREE.Vector3();
  private spin = 0;
  private toastSeconds = 0;

  knife: Knife = newKnife();
  /** How badly the jar has separated right now. 0 = mixed, 1 = oil on top. */
  strat = 0;
  private dipsSinceStir = 0;
  private stirPrev: number | null = null;
  private wasDipping = false;
  private knifeRig: KnifeRig;
  private pot: THREE.Group | null = null;
  private potHit: THREE.Mesh | null = null;
  private ray = new THREE.Raycaster();
  private hitPoint = new THREE.Vector3();
  private uv = new THREE.Vector2();
  private plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
  lastFx: StrokeFx | null = null;

  private camPos = new THREE.Vector3();
  private camLook = new THREE.Vector3();
  private shot: CamShot = SHOTS.toast;

  private panel: Panel;
  private cueEl!: HTMLElement;
  private hintEl!: HTMLElement;
  private dialRow!: HTMLElement;
  private gaugeFill!: HTMLElement;
  private gaugeYield!: HTMLElement;
  private gaugeScrape!: HTMLElement;
  private modeEl!: HTMLElement;
  private loadFill!: HTMLElement;
  private jarRow!: HTMLElement;
  private jarFill!: HTMLElement;
  private breadSel!: HTMLSelectElement;
  private spreadSel!: HTMLSelectElement;
  private toolSel!: HTMLSelectElement;
  private softInput!: HTMLInputElement;
  private softVal!: HTMLElement;

  /** Fired when the player hands the plate over. */
  onServed: (() => void) | null = null;
  /** Fired once the slice has landed on the plate — the drawer opens here. */
  onPopped: (() => void) | null = null;

  constructor(
    private app: App,
    seed = 1,
  ) {
    this.rng = new Rng(seed);
    this.root.add(makeBench());
    this.toaster = makeToaster();
    this.toaster.root.position.set(TOASTER_X, 0, -0.35);
    this.root.add(this.toaster.root);
    const plate = makePlate();
    plate.position.set(PLATE_X, 0, PLATE_Z);
    this.root.add(plate);

    this.knifeRig = new KnifeRig(TOOLS[this.toolId]);
    this.knifeRig.root.visible = false;
    this.root.add(this.knifeRig.root);

    this.camPos.copy(SHOTS.toast.pos);
    this.camLook.copy(SHOTS.toast.look);

    this.panel = new Panel('toast-panel');
    this.buildUi();
    this.loadBread('white');
  }

  private buildUi(): void {
    const p = this.panel.root;
    this.dialRow = el('div', 'row', p);
    el('label', 'lbl', this.dialRow, 'BROWNING');
    const dial = el('input', 'dial', this.dialRow);
    dial.type = 'range';
    dial.min = '1';
    dial.max = '10';
    dial.step = '1';
    dial.value = String(this.power);
    const val = el('span', 'val', this.dialRow, String(this.power));
    dial.addEventListener('input', () => {
      this.power = +dial.value;
      val.textContent = dial.value;
      this.toaster.dial.rotation.x = -((this.power - 1) / 9) * 2.4;
    });

    const row2 = el('div', 'row', p);
    el('label', 'lbl', row2, 'BREAD');
    const breadSel = el('select', 'sel', row2);
    this.breadSel = breadSel;
    for (const id of Object.keys(BREADS)) {
      const o = el('option', undefined, breadSel, BREADS[id as BreadId].name);
      o.value = id;
    }
    breadSel.addEventListener('change', () => this.loadBread(breadSel.value as BreadId));

    const row3 = el('div', 'row', p);
    el('label', 'lbl', row3, 'SPREAD');
    const spreadSel = el('select', 'sel', row3);
    this.spreadSel = spreadSel;
    for (const id of Object.keys(SPREADS)) {
      const o = el('option', undefined, spreadSel, SPREADS[id as SpreadId].name);
      o.value = id;
    }
    spreadSel.addEventListener('change', () => this.setSpread(spreadSel.value as SpreadId));

    const row4 = el('div', 'row', p);
    el('label', 'lbl', row4, 'TOOL');
    const toolSel = el('select', 'sel', row4);
    this.toolSel = toolSel;
    for (const id of Object.keys(TOOLS)) {
      const o = el('option', undefined, toolSel, TOOLS[id as ToolId].name);
      o.value = id;
    }
    toolSel.addEventListener('change', () => {
      this.toolId = toolSel.value as ToolId;
      this.knifeRig.setTool(TOOLS[this.toolId]);
    });

    const row5 = el('div', 'row', p);
    el('label', 'lbl', row5, 'BUTTER');
    const soft = el('input', 'dial', row5);
    this.softInput = soft;
    soft.type = 'range';
    soft.min = '0';
    soft.max = '100';
    soft.value = String(this.butterSoftness * 100);
    const softVal = el('span', 'val', row5, 'hard');
    this.softVal = softVal;
    soft.addEventListener('input', () => {
      this.butterSoftness = +soft.value / 100;
      softVal.textContent =
        this.butterSoftness > 0.66 ? 'soft' : this.butterSoftness > 0.33 ? 'ok' : 'hard';
    });

    this.jarRow = el('div', 'row', p);
    el('label', 'lbl', this.jarRow, 'JAR');
    const jarBar = el('div', 'load', this.jarRow);
    jarBar.style.flex = '1';
    this.jarFill = el('div', 'load-fill', jarBar);

    // The pressure gauge — the mechanic made legible. The fill is the pressure
    // your wrist angle is producing; the gold mark is what this spread needs in
    // order to flow; the red mark is where the knife stops spreading and starts
    // scraping. When gold sits past red, no angle can win, and the answer isn't
    // technique — it's warmer toast.
    const gauge = el('div', 'gauge', p);
    this.gaugeFill = el('div', 'gauge-fill', gauge);
    this.gaugeYield = el('div', 'gauge-mark yield', gauge);
    this.gaugeScrape = el('div', 'gauge-mark scrape', gauge);
    this.modeEl = el('div', 'mode', p, '');

    const loadBar = el('div', 'load', p);
    this.loadFill = el('div', 'load-fill', loadBar);

    this.cueEl = el('div', 'cue', p, 'smells like bread');
    this.hintEl = el('div', 'hint', p, 'SPACE — push the lever down');
    this.setSpread(this.spreadId);
  }

  setSpread(id: SpreadId): void {
    this.spreadId = id;
    const def = SPREADS[id];
    this.strat = def.separates ?? 0;
    this.dipsSinceStir = 0;
    this.stirPrev = null;
    this.wasDipping = false;
    this.slice?.setSpread(def);
    this.knifeRig.setSpread(def);
    if (this.pot) this.root.remove(this.pot);
    const { root, hit } = makeSpreadPot(def);
    root.position.set(POT_X, 0, POT_Z);
    this.pot = root;
    this.potHit = hit;
    this.pot.visible = this.phase === 'spreading';
    this.root.add(root);
  }

  loadBread(id: BreadId): void {
    if (this.slice) {
      this.root.remove(this.slice.mesh);
      this.slice.dispose();
    }
    this.slice = new Slice(BREADS[id], new Rng(this.rng.int(1, 1e6)));
    this.slice.setSpread(SPREADS[this.spreadId]);
    this.heat = buildHeatMap(this.slice, new Rng(this.rng.int(1, 1e6)));
    this.root.add(this.slice.mesh);
    this.phase = 'ready';
    this.leverT = 0;
    this.toastSeconds = 0;
    this.slice.warmth = 0;
    this.knife = newKnife();
    this.knifeRig.root.visible = false;
    if (this.pot) this.pot.visible = false;
    this.shot = SHOTS.toast;
    this.dialRow.style.opacity = '1';
    this.placeInSlot(0);
    this.hintEl.textContent = 'SPACE — push the lever down';
  }

  get currentSlice(): Slice {
    return this.slice;
  }

  /** Set the bench up for one order. The tool comes from the drawer (M4). */
  applyOrder(o: Order): void {
    this.butterSoftness = o.butterSoftness;
    this.toolId = o.tool;
    this.knifeRig.setTool(TOOLS[this.toolId]);
    this.setSpread(o.spread);
    this.loadBread(o.bread);
    this.breadSel.value = o.bread;
    this.spreadSel.value = o.spread;
    this.toolSel.value = o.tool;
    this.softInput.value = String(Math.round(o.butterSoftness * 100));
    this.softVal.textContent =
      o.butterSoftness > 0.66 ? 'soft' : o.butterSoftness > 0.33 ? 'ok' : 'hard';
  }

  serve(): void {
    if (this.phase !== 'spreading') return;
    this.onServed?.();
  }

  /**
   * What the next scoop out of the jar is like. The first dip off a split jar
   * takes the oil slick; the ones after it dig into what the oil left behind.
   * Stirred (strat 0), every dip is 0.5 — exactly right.
   */
  private jarOil(): number {
    if (this.strat <= 0.001) return 0.5;
    const swing = 0.5 * this.strat;
    // dip 0: +swing (the slick) · dip 1: about even · dip 2+: increasingly dry
    const t = 1 - this.dipsSinceStir * 0.55;
    return 0.5 + swing * Math.max(-1, Math.min(1, t));
  }

  /** Whatever you managed to pull out of the drawer is what you're spreading with. */
  setTool(id: ToolId): void {
    this.toolId = id;
    this.toolSel.value = id;
    this.knifeRig.setTool(TOOLS[id]);
  }

  flash(msg: string): void {
    this.hintEl.textContent = msg;
    this.hintEl.style.color = '#e2603a';
    window.setTimeout(() => {
      this.hintEl.style.color = '';
      this.hintEl.textContent = 'dip · drag · WHEEL tilts the knife · ENTER serves it';
    }, 2600);
  }

  private placeInSlot(t: number): void {
    const y = this.toaster.slotY + 0.16 - t * 0.74;
    this.slice.mesh.position.set(TOASTER_X, y, -0.35);
    this.slice.mesh.rotation.set(Math.PI / 2, 0, 0);
  }

  enter(): void {
    this.panel.show();
  }
  exit(): void {
    this.panel.hide();
  }

  private pushDown(): void {
    if (this.phase !== 'ready') return;
    audio.resume();
    audio.clunk();
    this.phase = 'toasting';
    this.hintEl.textContent = 'SPACE — pop it';
  }

  private pop(): void {
    if (this.phase !== 'toasting') return;
    this.phase = 'flying';
    this.slice.warmth = 1;
    this.toaster.setGlow(0);
    const p0 = this.slice.mesh.position.clone();
    const y1 = PLATE_Y + 0.02;
    const vy = 4.2;
    const g = 9.8;
    const disc = Math.max(0.01, vy * vy - 2 * g * (y1 - p0.y));
    const T = (vy + Math.sqrt(disc)) / g;
    this.vel.set((PLATE_X - p0.x) / T, vy, (PLATE_Z - p0.z) / T);
    this.spin = Math.PI / 2 / T;
    this.app.shake = 0.045;
    this.hintEl.textContent = '';
  }

  beginSpreading(): void {
    this.phase = 'spreading';
    this.shot = SHOTS.spread;
    this.knifeRig.root.visible = true;
    if (this.pot) this.pot.visible = true;
    this.dialRow.style.opacity = '0.35';
    this.hintEl.textContent = 'dip · drag · WHEEL tilts the knife · ENTER serves it';
  }

  update(dt: number): void {
    const inp = this.app.input;
    if (inp.justPressed('Space')) {
      if (this.phase === 'ready') this.pushDown();
      else if (this.phase === 'toasting') this.pop();
    }
    if (inp.justPressed('Enter') && this.phase === 'spreading') this.serve();

    const target = this.phase === 'ready' ? 0 : this.phase === 'toasting' ? 1 : 0;
    this.leverT += (target - this.leverT) * Math.min(1, dt * 14);
    this.toaster.lever.position.y = 1.62 * 0.72 - this.leverT * 0.62;

    if (this.phase === 'toasting') {
      this.placeInSlot(this.leverT);
      this.toastSeconds += dt;
      toastStep(this.slice, this.heat, this.power, dt);
      this.toaster.setGlow(
        0.35 + 0.65 * (this.power / 10) * (0.92 + Math.sin(this.toastSeconds * 9) * 0.08),
      );
      this.slice.setHeatGlow(0.55 + 0.45 * Math.sin(this.toastSeconds * 7) * 0.3);
    } else if (this.phase === 'ready') {
      this.placeInSlot(this.leverT);
    } else if (this.phase === 'flying') {
      this.vel.y -= 9.8 * dt;
      this.slice.mesh.position.addScaledVector(this.vel, dt);
      this.slice.mesh.rotation.x = Math.max(0, this.slice.mesh.rotation.x - this.spin * dt);
      this.slice.mesh.rotation.z += dt * 1.1;
      this.slice.setHeatGlow(0);
      if (this.slice.mesh.position.y <= PLATE_Y + 0.02 && this.vel.y < 0) {
        this.slice.mesh.position.set(PLATE_X, PLATE_Y + 0.02, PLATE_Z);
        this.slice.mesh.rotation.set(0, 0, 0);
        this.app.shake = 0.02;
        this.phase = 'landed';
        audio.clunk(true);
        if (this.onPopped) this.onPopped();
        else this.beginSpreading();
      }
    } else if (this.phase === 'spreading') {
      this.updateSpreading(dt);
    }

    this.updateCamera(dt);

    if (this.phase !== 'spreading') {
      const t = readToast(this.slice);
      this.cueEl.textContent = smellCue(t);
      this.cueEl.style.color = t.smoke > 0.45 ? '#e2603a' : '';
    }
    this.slice.sync();
  }

  private updateSpreading(dt: number): void {
    const inp = this.app.input;
    const def = SPREADS[this.spreadId];
    const tool = TOOLS[this.toolId];

    if (inp.wheel !== 0) {
      this.knife.angle = Math.max(0, Math.min(1, this.knife.angle + inp.wheel * 0.055));
    }

    this.ray.setFromCamera(inp.ndc, this.app.camera);

    // Dipping wins over spreading: if the cursor is over the pot you're loading up.
    let overPot = false;
    if (this.potHit) {
      const hits = this.ray.intersectObject(this.potHit, false);
      if (hits.length) {
        overPot = true;
        this.knifeRig.update(hits[0].point, this.knife.angle * 0.3, this.knife.load, hits[0].point.y);
        if (inp.down) {
          // Swirling: angular travel of the knife around the jar's centre. Only
          // sweep counts — holding still in the jar stirs nothing.
          if (def.separates && this.strat > 0.001) {
            const ang = Math.atan2(hits[0].point.z - POT_Z, hits[0].point.x - POT_X);
            if (this.stirPrev !== null) {
              let d = ang - this.stirPrev;
              if (d > Math.PI) d -= Math.PI * 2;
              if (d < -Math.PI) d += Math.PI * 2;
              // ~2.5 full circles takes the jar from fully split to mixed.
              const sweep = Math.abs(d);
              if (sweep > 0.002) {
                this.strat = Math.max(0, this.strat - sweep / (5 * Math.PI));
                this.dipsSinceStir = 0;
                if (sweep > 0.02) audio.scrape(0.25, sweep * 6);
              }
            }
            this.stirPrev = ang;
          }
          // A dip is an event, not a state: the oil you get depends on how many
          // scoops have already come off the top since the last stir.
          if (!this.wasDipping) {
            dip(this.knife, def, this.jarOil());
            this.dipsSinceStir++;
            this.wasDipping = true;
          } else {
            // Holding the blade in the jar keeps it topped up at the same blend.
            dip(this.knife, def, this.knife.oil);
          }
        } else {
          this.stirPrev = null;
          this.wasDipping = false;
        }
        this.knife.hasPrev = false;
      }
    }
    if (!overPot) {
      this.stirPrev = null;
      this.wasDipping = false;
    }

    if (!overPot) {
      this.plane.constant = -this.slice.topY;
      if (this.ray.ray.intersectPlane(this.plane, this.hitPoint)) {
        this.knifeRig.update(this.hitPoint, this.knife.angle, this.knife.load, this.slice.topY);
        this.slice.uvAt(this.hitPoint, this.uv);
        const onBread = this.slice.mask.sample(this.uv.x, this.uv.y) > 0.5;
        if (inp.down && onBread) {
          this.lastFx = stroke(
            this.slice,
            def,
            tool,
            this.knife,
            this.uv.x,
            this.uv.y,
            dt,
            this.butterSoftness,
          );
          if (this.lastFx.tore > 0.004 || this.lastFx.gouged > 0.004) this.app.shake = 0.012;
          if (this.lastFx.mode !== 'idle' && this.lastFx.speed > 0.15) {
            audio.scrape(this.lastFx.pressure, this.lastFx.speed);
          }
        } else {
          this.knife.hasPrev = false;
          this.lastFx = null;
        }
      }
    }

    this.slice.uniforms.uOil.value = def.separates ? this.slice.oilMean : 0.5;
    this.updateGauge(def);
  }

  private updateGauge(def: SpreadDef): void {
    const tool = TOOLS[this.toolId];
    const pressure = Math.min(1.4, pressureFromAngle(this.knife.angle) / tool.contactScale);
    const yieldP = effectiveYield(def, this.butterSoftness, this.slice.warmth);
    const scrapeP = Math.min(1.4, pressureFromAngle(SCRAPE_ANGLE) / tool.contactScale);
    const pct = (v: number) => `${Math.min(100, (v / 1.4) * 100)}%`;
    this.gaugeFill.style.width = pct(pressure);
    this.gaugeYield.style.left = pct(yieldP);
    this.gaugeScrape.style.left = pct(scrapeP);
    const scraping = this.knife.angle >= SCRAPE_ANGLE;
    this.gaugeFill.style.background = scraping
      ? '#c0392b'
      : pressure >= yieldP
        ? '#6cbf6c'
        : '#8a7f70';

    this.jarRow.style.display = def.separates ? '' : 'none';
    if (def.separates) {
      const mixed = 1 - this.strat;
      this.jarFill.style.width = `${Math.round(mixed * 100)}%`;
      this.jarFill.style.background = mixed > 0.75 ? '#6cbf6c' : mixed > 0.4 ? '#e8a53a' : '#c0392b';
    }

    const fx = this.lastFx;
    let msg = scraping ? 'scraping' : 'spreading';
    if (fx) {
      if (fx.mode === 'tear') msg = 'TEARING — the butter is too hard for this';
      else if (fx.mode === 'gouge') msg = 'GOUGING — there is nothing left to scrape';
      else if (fx.mode === 'char') msg = 'lifting the burnt bits';
      else if (fx.mode === 'scrape') msg = 'taking it back off';
      else if (fx.mode === 'spread') msg = 'spreading nicely';
    } else if (this.knife.load <= 0.01) {
      msg =
        def.separates && this.strat > 0.45
          ? 'the oil has risen — swirl the knife in the jar first'
          : 'the knife is empty — go and dip';
    }
    this.modeEl.textContent = msg;
    this.modeEl.style.color =
      fx?.mode === 'tear' || fx?.mode === 'gouge'
        ? '#e2603a'
        : fx?.mode === 'spread'
          ? '#8fce8f'
          : '';
    this.loadFill.style.width = `${Math.min(100, (this.knife.load / def.pickup) * 100)}%`;
    this.loadFill.style.background = `rgb(${def.color.map((c) => Math.round(c * 255)).join(',')})`;
  }

  private updateCamera(dt: number): void {
    const k = 1 - Math.pow(0.0035, dt);
    this.camPos.lerp(this.shot.pos, k);
    this.camLook.lerp(this.shot.look, k);
    this.app.camera.position.copy(this.camPos);
    this.app.camera.lookAt(this.camLook);
  }

  dispose(): void {
    this.panel.dispose();
    this.slice.dispose();
  }
}
