import Phaser from 'phaser';
import type { EventBus } from '../core/EventBus';
import { UI, chunkyButton, font, panel } from './style';

export interface ClickerSfx {
  clickerClunk(): void;
}

export interface ClickerOptions {
  x: number;
  y: number;
  sfx?: ClickerSfx;
  depth?: number;
}

const BODY_W = 52;
const BODY_H = 34;

// Recessed digit window, in body-local coords.
const WIN_X = 0;
const WIN_Y = -9;
const WIN_W = 34;
const WIN_H = 15;

const DIGIT_SPACING = 10;
const ROLL_MS = 120;
// Slide distance: a glyph must clear the window before the mask stops hiding it.
const ROLL_TRAVEL = WIN_H;

const MAX_COUNT = 999;

interface DigitSlot {
  readonly holder: Phaser.GameObjects.Container;
  current: Phaser.GameObjects.Text;
  incoming: Phaser.GameObjects.Text | null;
  t: number;
}

/**
 * The handheld tally clicker. It owns the SHOWN number only — how many people
 * are actually inside is somebody else's problem, which is the entire joke. IN
 * is fat and obvious, OUT is small and easy to forget.
 */
export class Clicker {
  private readonly scene: Phaser.Scene;
  private readonly bus: EventBus;
  private readonly sfx: ClickerSfx | undefined;
  private readonly root: Phaser.GameObjects.Container;
  private readonly maskGfx: Phaser.GameObjects.Graphics;
  private readonly slots: DigitSlot[] = [];
  private shown = 0;

  constructor(scene: Phaser.Scene, bus: EventBus, opts: ClickerOptions) {
    this.scene = scene;
    this.bus = bus;
    this.sfx = opts.sfx;

    this.root = scene.add.container(opts.x, opts.y).setDepth(opts.depth ?? 900);

    const body = panel(scene, 0, 0, BODY_W, BODY_H, UI.grime);
    const well = scene.add.rectangle(WIN_X, WIN_Y, WIN_W, WIN_H, UI.ink).setStrokeStyle(1, UI.panelLip);
    this.root.add([body, well]);

    // Geometry mask lives in world space, so the widget is not meant to move
    // after construction — nothing in the door scene moves it.
    this.maskGfx = scene.make.graphics({}, false);
    this.maskGfx.fillStyle(0xffffff);
    this.maskGfx.fillRect(
      opts.x + WIN_X - WIN_W / 2,
      opts.y + WIN_Y - WIN_H / 2,
      WIN_W,
      WIN_H,
    );
    const mask = this.maskGfx.createGeometryMask();

    for (let i = 0; i < 3; i++) {
      const holder = scene.add.container(WIN_X + (i - 1) * DIGIT_SPACING, WIN_Y);
      holder.setMask(mask);
      const current = this.makeGlyph('0');
      holder.add(current);
      this.root.add(holder);
      this.slots.push({ holder, current, incoming: null, t: 0 });
    }

    this.root.add(
      chunkyButton(scene, -12, 10, 'IN', {
        w: 26,
        h: 13,
        fill: UI.ok,
        fontSize: 9,
        onClick: () => this.bump(1),
      }),
    );
    this.root.add(
      chunkyButton(scene, 14, 11, 'out', {
        w: 18,
        h: 9,
        fill: UI.panelLip,
        textColour: UI.dim,
        fontSize: 6,
        onClick: () => this.bump(-1),
      }),
    );
  }

  get count(): number {
    return this.shown;
  }

  update(deltaMs: number): void {
    for (const slot of this.slots) {
      const incoming = slot.incoming;
      if (!incoming) continue;
      slot.t += deltaMs;
      const p = Math.min(1, slot.t / ROLL_MS);
      const e = Phaser.Math.Easing.Quadratic.Out(p);
      slot.current.y = -ROLL_TRAVEL * e;
      incoming.y = ROLL_TRAVEL * (1 - e);
      if (p >= 1) this.settle(slot);
    }
  }

  destroy(): void {
    for (const slot of this.slots) slot.holder.clearMask();
    this.maskGfx.destroy();
    this.root.destroy();
  }

  private makeGlyph(glyph: string): Phaser.GameObjects.Text {
    return this.scene.add.text(0, 0, glyph, font(12, UI.neonGreen)).setOrigin(0.5);
  }

  /**
   * The clunk is mechanical — it fires even at the stops. The event does not:
   * core/meters tracks clickerShown from the same event, so a press that moves
   * nothing must not move it either.
   */
  private bump(delta: number): void {
    this.sfx?.clickerClunk();
    const next = Phaser.Math.Clamp(this.shown + delta, 0, MAX_COUNT);
    if (next === this.shown) return;
    this.shown = next;
    this.bus.emit('door:clicker', { direction: delta > 0 ? 'in' : 'out' });
    this.render(next);
  }

  private render(value: number): void {
    const digits = value.toString().padStart(3, '0');
    for (let i = 0; i < this.slots.length; i++) {
      const slot = this.slots[i];
      const glyph = digits[i];
      if (!slot || glyph === undefined) continue;
      const settled = slot.incoming ?? slot.current;
      if (settled.text === glyph) continue;
      this.roll(slot, glyph);
    }
  }

  private roll(slot: DigitSlot, glyph: string): void {
    // A press mid-roll snaps the in-flight glyph home rather than stacking.
    if (slot.incoming) this.settle(slot);
    const incoming = this.makeGlyph(glyph);
    incoming.y = ROLL_TRAVEL;
    slot.holder.add(incoming);
    slot.incoming = incoming;
    slot.t = 0;
  }

  private settle(slot: DigitSlot): void {
    const incoming = slot.incoming;
    if (!incoming) return;
    slot.current.destroy();
    incoming.y = 0;
    slot.current = incoming;
    slot.incoming = null;
    slot.t = 0;
  }
}
