import type { Slice } from './slice';
import type { SpreadDef } from './spreads';
import { SCRAPE_ANGLE, contactRadius, effectiveYield, pressureFromAngle } from './spreads';
import type { Tool } from './cutlery';

/**
 * The star mechanic.
 *
 * One control — knife angle — and three behaviours fall out of it:
 *
 *   flat   -> wide contact, low pressure  -> spreads (or tears, if the spread is
 *             stiffer than the pressure you're allowed to apply)
 *   steep  -> narrow contact, high pressure -> scrapes spread back off, or scrapes
 *             char off toast that's burnt
 *   steep and fast on bread that has nothing left to remove -> gouges
 *
 * The trap: pressure comes from steepness, but steepness past SCRAPE_ANGLE stops
 * spreading. So cold butter can demand more pressure than the spread mode can
 * physically give — and the answer isn't technique, it's warmer toast.
 */

/** Scraping lightens toast down to here; past that you're into the crumb. */
export const BROWN_FLOOR = 0.62;

const TEAR_RATE = 1.15;
const SCRAPE_RATE = 2.4;
/**
 * Lifting char has to feel like progress. A steep knife has a narrow contact
 * patch (~0.05 UV), so a pass only dwells on any given texel for ~0.2s — at a
 * lower rate than this you scrape a burnt slice for a minute and it stays burnt.
 */
const CHAR_SCRAPE_RATE = 4.0;
const GOUGE_RATE = 0.85;
/** Below this pressure you're just wiping; you can't gouge with a flat knife. */
const GOUGE_PRESSURE = 0.5;
/** Gouging and tearing both need the knife to be moving. */
const MOVE_REF = 0.55;

export interface Knife {
  /** 0 = flat on the toast, 1 = up on its edge. */
  angle: number;
  /** Spread carried on the blade, in mean-thickness units (same as the field). */
  load: number;
  /** Solid bits (rind) riding the blade, in whole pieces. */
  rindLoad: number;
  /** Fractional shed accumulator — pieces drop one at a time. */
  rindFrac: number;
  /**
   * Consistency of what's on the blade: 0.5 is properly mixed, 1 is the oil
   * slick off the top of an unstirred jar, 0 is the dried-out paste under it.
   */
  oil: number;
  u: number;
  v: number;
  hasPrev: boolean;
}

export type StrokeMode = 'idle' | 'spread' | 'tear' | 'scrape' | 'char' | 'gouge';

export interface StrokeFx {
  mode: StrokeMode;
  deposited: number;
  tore: number;
  gouged: number;
  scrapedSpread: number;
  scrapedChar: number;
  speed: number;
  pressure: number;
  /** How far short of the yield pressure we are: 0 = flowing, 1 = hopeless. */
  deficit: number;
  crumbs: number;
}

export function newKnife(): Knife {
  // Starts inside the window where warm-toast butter actually flows: the very
  // first stroke of the game shouldn't be a mysterious failure.
  return { angle: 0.45, load: 0, rindLoad: 0, rindFrac: 0, oil: 0.5, u: 0.5, v: 0.5, hasPrev: false };
}

/** `oil` is what the jar handed over — 0.5 unless the jar has stratified. */
export function dip(knife: Knife, def: SpreadDef, oil = 0.5): void {
  knife.load = def.pickup;
  knife.oil = oil;
  if (def.particles) {
    knife.rindLoad = def.particles.perDip;
    knife.rindFrac = 0;
  }
}

/** Cheap per-texel hash, for blotchy tools. */
function blotchAt(i: number): number {
  const x = Math.sin(i * 12.9898) * 43758.5453;
  return x - Math.floor(x);
}

/**
 * Drag the knife from where it was to (toU,toV). Substeps along the path so a
 * fast flick can't skip over the middle of the slice.
 */
export function stroke(
  slice: Slice,
  def: SpreadDef,
  tool: Tool,
  knife: Knife,
  toU: number,
  toV: number,
  dt: number,
  softness: number,
): StrokeFx {
  const fx: StrokeFx = {
    mode: 'idle',
    deposited: 0,
    tore: 0,
    gouged: 0,
    scrapedSpread: 0,
    scrapedChar: 0,
    speed: 0,
    pressure: 0,
    deficit: 0,
    crumbs: 0,
  };
  if (!knife.hasPrev) {
    knife.u = toU;
    knife.v = toV;
    knife.hasPrev = true;
    return fx;
  }
  const du = toU - knife.u;
  const dv = toV - knife.v;
  const dist = Math.hypot(du, dv);
  const speed = dt > 0 ? dist / dt : 0;
  fx.speed = speed;

  const radius = Math.max(0.012, contactRadius(knife.angle) * tool.contactScale);
  // Pressure is force over area: the same wrist angle through a narrow blade
  // presses harder than through a wide one.
  const pressure = Math.min(1.4, pressureFromAngle(knife.angle) / tool.contactScale);
  fx.pressure = pressure;

  const steps = Math.max(1, Math.min(24, Math.ceil(dist / (radius * 0.45))));
  const stepDt = dt / steps;
  for (let s = 1; s <= steps; s++) {
    const t = s / steps;
    contact(slice, def, tool, knife, knife.u + du * t, knife.v + dv * t, pressure, radius, speed, stepDt, softness, fx);
  }
  knife.u = toU;
  knife.v = toV;

  // Low-viscosity spreads level themselves out; peanut butter never does.
  const relax = (1 - def.viscosity) * dt * 3.2;
  if (relax > 0.001) slice.spread.relax(Math.min(0.5, relax), slice.mask);

  slice.touch();
  return fx;
}

function contact(
  slice: Slice,
  def: SpreadDef,
  tool: Tool,
  knife: Knife,
  u: number,
  v: number,
  pressure: number,
  radius: number,
  speed: number,
  dt: number,
  softness: number,
  fx: StrokeFx,
): void {
  const mask = slice.mask.data;
  const spread = slice.spread.data;
  const brown = slice.browning.data;
  const damage = slice.damage.data;
  const moving = Math.min(1.6, speed / MOVE_REF);

  if (knife.angle < SCRAPE_ANGLE) {
    if (knife.load <= 0.0005) return;
    // Consistency pushes back through the same physics as everything else:
    // an oily load flows at almost no pressure, a dried-out one fights like
    // cold butter. Only spreads that separate care.
    const oilK = def.separates ? 1.25 - knife.oil * 0.5 : 1;
    const yieldP = effectiveYield(def, softness, slice.warmth) * oilK;
    const flowing = pressure >= yieldP;
    const deficit = flowing ? 0 : Math.min(1, (yieldP - pressure) / Math.max(yieldP, 0.001));
    fx.deficit = Math.max(fx.deficit, deficit);
    fx.mode = flowing ? 'spread' : 'tear';

    // Below the yield pressure the spread won't flow, so the blade grabs the
    // crumb and takes it with it. Some still smears on, badly.
    const transfer = flowing ? 1 : 0.25;
    // Runny goes on thin, dry goes on in slabs.
    const oilRate = def.separates ? 1.15 - knife.oil * 0.4 : 1;
    const rate = def.transferRate * tool.transferScale * dt * transfer * oilRate;
    let massLeft = knife.load * slice.mask.n * slice.mask.n;

    let laid = 0;
    slice.spread.brush(u, v, radius, (i, w) => {
      if (mask[i] < 0.5 || massLeft <= 0) return;
      const blot = 1 - tool.blotch * blotchAt(i);
      const add = Math.min(rate * w * blot, massLeft);
      spread[i] += add;
      massLeft -= add;
      laid += add;
      fx.deposited += add;
    });
    knife.load = Math.max(0, massLeft / (slice.mask.n * slice.mask.n));
    // What lands remembers the consistency it landed at — the judge reads the
    // mass-weighted mean at the end.
    slice.recordDeposit(laid, knife.oil);

    // Suspended solids drop off the blade as it works — per unit of stroke
    // DISTANCE, not per second. That's the whole feel: a moving knife lays an
    // even trail, a dabbing knife piles everything into one spot, and because
    // the rate scales with what's left on the blade, one dip can't cover the
    // slice — even rind takes several dips, placed with intent.
    if (def.particles && knife.rindLoad > 0.01) {
      knife.rindFrac += knife.rindLoad * def.particles.shedRate * speed * dt;
      while (knife.rindFrac >= 1 && knife.rindLoad >= 1) {
        knife.rindFrac -= 1;
        knife.rindLoad -= 1;
        const h1 = blotchAt(slice.rind.length * 7 + 1);
        const h2 = blotchAt(slice.rind.length * 13 + 5);
        const ang = h1 * Math.PI * 2;
        const dist = Math.sqrt(h2) * radius * 0.8;
        const ru = u + Math.cos(ang) * dist;
        const rv = v + Math.sin(ang) * dist;
        if (slice.mask.sample(ru, rv) > 0.5) slice.addRind(ru, rv);
      }
    }

    if (!flowing && moving > 0.05) {
      const tear = TEAR_RATE * deficit * moving * tool.tearProne * dt;
      slice.damage.brush(u, v, radius, (i, w) => {
        if (mask[i] < 0.5) return;
        const add = tear * w;
        damage[i] = Math.min(1, damage[i] + add);
        fx.tore += add;
      });
      fx.crumbs += tear * 26;
    }
    return;
  }

  // --- scrape ---
  // The blade takes rind with it too — some of it survives the trip and can be
  // redistributed, which is how you fix a clump without starting over.
  const rindOff = slice.removeRindNear(u, v, radius * 1.15);
  if (rindOff > 0) knife.rindLoad += rindOff * 0.6;

  let removedSpread = 0;
  let removedChar = 0;
  let gouged = 0;
  slice.spread.brush(u, v, radius, (i, w) => {
    if (mask[i] < 0.5) return;
    if (spread[i] > 0.002) {
      const off = Math.min(spread[i], SCRAPE_RATE * pressure * def.scrapeEase * dt * w);
      spread[i] -= off;
      removedSpread += off;
      return;
    }
    if (brown[i] > BROWN_FLOOR) {
      // Lifting char off. Cheap in browning, expensive in evenness — the saved
      // patch ends up lighter than everything around it.
      const off = Math.min(brown[i] - BROWN_FLOOR, CHAR_SCRAPE_RATE * pressure * dt * w);
      brown[i] -= off;
      removedChar += off;
      return;
    }
    // Nothing left to take but the bread itself.
    if (pressure > GOUGE_PRESSURE && moving > 0.25) {
      const add = GOUGE_RATE * (pressure - GOUGE_PRESSURE) * moving * tool.gougeProne * dt * w;
      damage[i] = Math.min(1, damage[i] + add);
      gouged += add;
    }
  });

  // What comes off the toast mostly stays on the blade.
  knife.load += (removedSpread * 0.6) / (slice.mask.n * slice.mask.n);
  slice.recordRemoval(removedSpread);
  fx.scrapedSpread += removedSpread;
  fx.scrapedChar += removedChar;
  fx.gouged += gouged;
  fx.crumbs += removedChar * 900 + gouged * 30;
  fx.mode = gouged > 1e-6 ? 'gouge' : removedChar > 1e-6 ? 'char' : removedSpread > 1e-6 ? 'scrape' : 'idle';
}
