import Phaser from 'phaser';
import { UI, font, shake } from './style';

// The stamp is the whole game in one gesture, so the timing is doing the acting:
// a wind-up above the target, 160ms of accelerating travel, a hard hit, then a
// 90ms dead hang before the lift. The hang is what makes it feel like a decision
// rather than an animation — do not shorten it.

const HEAD_W = 54;
const HEAD_H = 22;

const TRAVEL_MS = 160;
const HANG_MS = 90;
const RELEASE_MS = 180;
const RING_MS = 200;
const DUST_MS = 350;

const DUST_COUNT = 9;
const SPLATS = 7;

/** Head sits above the decal; ring and dust sit between them. */
const RING_DEPTH_OFFSET = 5;
const HEAD_DEPTH_OFFSET = 10;

export interface StampSfx {
  stampSlam(): void;
  stampInk(): void;
}

export interface StampHandle {
  /** The ink mark left behind. The caller owns it; destroy it when the patron leaves. */
  decal: Phaser.GameObjects.Container;
}

export interface StampOptions {
  /** Always scene/world coordinates — converted into `parent` space when one is given. */
  x: number;
  y: number;
  text?: string;
  colour?: number;
  depth?: number;
  sfx?: StampSfx;
  /** Parent for the decal, so the mark travels with the patron. Defaults to the scene root. */
  parent?: Phaser.GameObjects.Container;
  /** Per-impression variation. Same seed => same mark. Defaults to a hash of `text`. */
  seed?: number;
  onSlam?: () => void;
  onDone?: () => void;
}

/**
 * FNV-1a over the label so DENIED and ADMITTED get different splatter, stably; the
 * caller's seed is folded in after so two DENIEDs are not the same printed mess.
 */
function hashText(text: string, seed?: number): number {
  let h = 2166136261;
  for (const ch of text) {
    h ^= ch.charCodeAt(0);
    h = Math.imul(h, 16777619);
  }
  if (seed !== undefined) {
    let s = seed >>> 0;
    for (let i = 0; i < 4; i++) {
      h ^= s & 0xff;
      h = Math.imul(h, 16777619);
      s >>>= 8;
    }
  }
  return (h >>> 0) % 4096;
}

/** Hash noise in place of Math.random — same label always prints the same mess. */
function sprinkle(seed: number, i: number): number {
  const s = Math.sin(seed * 12.9898 + i * 78.233) * 43758.5453;
  return s - Math.floor(s);
}

/** The rubber head: dark body, inked face, knurled grip on top. */
function buildHead(
  scene: Phaser.Scene,
  colour: number,
  label: string,
): Phaser.GameObjects.Container {
  const head = scene.add.container(0, 0);

  const body = scene.add.graphics();
  body.fillStyle(UI.ink, 1);
  body.fillRoundedRect(-HEAD_W / 2, -HEAD_H / 2, HEAD_W, HEAD_H, 3);
  body.fillStyle(UI.panelLip, 1);
  body.fillRect(-10, -HEAD_H / 2 - 8, 20, 8);
  body.fillStyle(UI.panel, 1);
  body.fillRect(-14, -HEAD_H / 2 - 11, 28, 4);

  const face = scene.add.graphics();
  face.fillStyle(colour, 1);
  face.fillRoundedRect(-HEAD_W / 2 + 3, -HEAD_H / 2 + 4, HEAD_W - 6, HEAD_H - 7, 2);

  const text = scene.add.text(0, 1, label, font(8, UI.paper)).setOrigin(0.5).setLetterSpacing(1);

  head.add([body, face, text]);
  return head;
}

/** The mark that outlives the gesture: border, label, and uneven ink. */
function fillDecal(
  scene: Phaser.Scene,
  decal: Phaser.GameObjects.Container,
  colour: number,
  label: string,
  seed: number,
): void {
  // Never quite square to the card — a machine would print it straight.
  decal.setRotation((sprinkle(seed, 0) - 0.5) * 0.16);

  const border = scene.add.graphics();
  border.lineStyle(2, colour, 0.85);
  border.strokeRoundedRect(-HEAD_W / 2 + 2, -HEAD_H / 2 + 3, HEAD_W - 4, HEAD_H - 6, 2);
  decal.add(border);

  // Smudge under the crisp pass: one bad impression, printed twice.
  decal.add(
    scene.add.text(1, 2, label, font(9, colour)).setOrigin(0.5).setLetterSpacing(1).setAlpha(0.35),
  );
  decal.add(scene.add.text(0, 1, label, font(9, colour)).setOrigin(0.5).setLetterSpacing(1).setAlpha(0.92));

  // Over-inked pooling near the edges plus a few thrown specks.
  for (let i = 0; i < SPLATS; i++) {
    const a = (i / SPLATS) * Math.PI * 2 + sprinkle(seed, 100 + i);
    const r = 12 + sprinkle(seed, 200 + i) * 16;
    const size = 1 + Math.round(sprinkle(seed, 300 + i) * 2);
    decal.add(
      scene.add
        .circle(Math.cos(a) * r, Math.sin(a) * r * 0.5, size, colour, 1)
        .setAlpha(0.28 + sprinkle(seed, 400 + i) * 0.35),
    );
  }
}

/** Shockwave: a flattened ring, because the ink lands on a surface, not in air. */
function spawnRing(scene: Phaser.Scene, opts: StampOptions, colour: number, depth: number): void {
  const ring = scene.add
    .circle(opts.x, opts.y, 10, colour, 0)
    .setStrokeStyle(2, colour, 0.9)
    .setDepth(depth)
    .setScale(0.4, 0.25);

  scene.tweens.add({
    targets: ring,
    scaleX: 2.4,
    scaleY: 1.3,
    alpha: 0,
    duration: RING_MS,
    ease: 'Cubic.easeOut',
    onComplete: () => ring.destroy(),
  });
}

/** Fixed angular steps — a random spray reads as noise, an even ring reads as force. */
function spawnDust(scene: Phaser.Scene, opts: StampOptions, colour: number, seed: number, depth: number): void {
  for (let i = 0; i < DUST_COUNT; i++) {
    const a = (i / DUST_COUNT) * Math.PI * 2;
    const dist = 13 + sprinkle(seed, 500 + i) * 11;
    const size = 1 + Math.round(sprinkle(seed, 600 + i));
    // Alternating grit and ink so the burst reads as both dust and spatter.
    const tint = i % 2 === 0 ? UI.paper : colour;

    const p = scene.add
      .rectangle(opts.x, opts.y, size, size, tint, 0.75)
      .setDepth(depth);

    scene.tweens.add({
      targets: p,
      x: opts.x + Math.cos(a) * dist,
      y: opts.y + Math.sin(a) * dist * 0.45 + 4,
      alpha: 0,
      duration: DUST_MS,
      ease: 'Quad.easeOut',
      onComplete: () => p.destroy(),
    });
  }
}

/**
 * Slam a stamp onto world (x, y). The head, ring and dust clean themselves up; the
 * returned decal is the caller's to destroy — nothing else will. It is empty until
 * the head lands, so the returned container is safe to reparent or destroy early.
 * Colour and label are pure inputs — the ADMITTED variant is the same code path.
 */
export function stamp(scene: Phaser.Scene, opts: StampOptions): StampHandle {
  const label = opts.text ?? 'DENIED';
  const colour = opts.colour ?? UI.danger;
  const depth = opts.depth ?? 0;
  const seed = hashText(label, opts.seed);

  const decal = scene.add.container(0, 0).setDepth(depth);
  if (opts.parent) opts.parent.add(decal);
  let decalAlive = true;
  decal.once(Phaser.GameObjects.Events.DESTROY, () => {
    decalAlive = false;
  });

  const head = buildHead(scene, colour, label);
  head
    .setDepth(depth + HEAD_DEPTH_OFFSET)
    .setPosition(opts.x + 14, opts.y - 70)
    .setScale(1.6)
    .setRotation(-0.35)
    .setAlpha(0.35);

  const impact = (): void => {
    shake(scene, 4, 120);
    opts.sfx?.stampSlam();
    opts.sfx?.stampInk();
    opts.onSlam?.();

    if (decalAlive) {
      // Placed at impact, not at call time, so a moving parent stamps where the ink hit.
      const local = opts.parent
        ? opts.parent.getWorldTransformMatrix().applyInverse(opts.x, opts.y)
        : { x: opts.x, y: opts.y };
      decal.setPosition(local.x, local.y);
      fillDecal(scene, decal, colour, label, seed);
    }
    spawnRing(scene, opts, colour, depth + RING_DEPTH_OFFSET);
    spawnDust(scene, opts, colour, seed, depth + RING_DEPTH_OFFSET);

    // `delay` is the hang — nothing moves, and that stillness is the punctuation.
    scene.tweens.add({
      targets: head,
      y: opts.y - 26,
      alpha: 0,
      duration: RELEASE_MS,
      delay: HANG_MS,
      ease: 'Quad.easeOut',
      onComplete: () => {
        head.destroy();
        opts.onDone?.();
      },
    });
  };

  // Split easings per axis: the horizontal settles early while the vertical is
  // still accelerating, which bends the path into an arc without a path object.
  scene.tweens.add({
    targets: head,
    x: opts.x,
    duration: TRAVEL_MS,
    ease: 'Quad.easeOut',
  });
  scene.tweens.add({
    targets: head,
    alpha: 1,
    duration: TRAVEL_MS * 0.6,
    ease: 'Linear',
  });
  scene.tweens.add({
    targets: head,
    y: opts.y,
    rotation: 0,
    scaleX: 1,
    scaleY: 1,
    duration: TRAVEL_MS,
    ease: 'Back.easeIn',
    onComplete: impact,
  });

  return { decal };
}
