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

export interface MeterHudOptions {
  x?: number;
  y?: number;
  depth?: number;
  /**
   * Strikes already on the licence when this HUD is built (review ruling: read
   * `NightState.heatStrikes.length` at construction — heat is run-scoped and a
   * mid-run scene swap must not reset the display to zero).
   */
  initialHeat?: number;
}

const BAR_X = 30;
const BAR_W = 84;
const BAR_H = 7;
const ROW_H = 11;
const SEGMENTS = 10;
const SEG_GAP = 1;
const SEG_W = (BAR_W - SEG_GAP * (SEGMENTS - 1)) / SEGMENTS;
const SLOT = 12;
const SLOT_GAP = 3;
const HEAT_SLOTS = 3;

// Below this the neon sign starts dying; above it the bar is rock steady.
const VIBE_FLICKER_BELOW = 25;
// Above this the crowd strip starts breathing.
const AGGRO_PULSE_ABOVE = 70;

const AGGRO_COOL = 0x2f6f8f;

const clamp01 = (v: number): number => (v < 0 ? 0 : v > 1 ? 1 : v);

function mix(a: number, b: number, t: number): number {
  const c = Phaser.Display.Color.Interpolate.ColorWithColor(
    Phaser.Display.Color.ValueToColor(a),
    Phaser.Display.Color.ValueToColor(b),
    100,
    Math.round(clamp01(t) * 100),
  );
  return Phaser.Display.Color.GetColor(c.r, c.g, c.b);
}

// Frame-rate independent ease: same settle time at 30fps as at 144.
const approach = (cur: number, target: number, deltaMs: number, tauMs: number): number =>
  target + (cur - target) * Math.exp(-deltaMs / tauMs);

interface HeatSlot {
  mark: Phaser.GameObjects.Container;
  scale: number;
}

/**
 * Vibe / Aggro / Hype / Heat readout. Read-only: it listens to `meters:changed`
 * and `heat:strike` and never emits or mutates anything. Each meter gets its own
 * visual language so a glance tells you which number moved.
 */
export class MeterHud {
  private readonly root: Phaser.GameObjects.Container;
  private readonly offs: Array<() => void> = [];

  private vibe = 50;
  private vibeShown = 50;
  private aggro = 0;
  private aggroShown = 0;
  private hype = 1;
  private hypeScale = 1;
  private strikes = 0;

  // Accumulated phases — deterministic stand-in for noise (no Math.random).
  private flickerPhase = 0;
  private pulsePhase = 0;

  private readonly vibeGlow: Phaser.GameObjects.Rectangle;
  private readonly vibeFill: Phaser.GameObjects.Rectangle;
  private readonly aggroStrip: Phaser.GameObjects.Container;
  private readonly segs: Phaser.GameObjects.Rectangle[] = [];
  private readonly hypeBadge: Phaser.GameObjects.Container;
  private readonly hypeText: Phaser.GameObjects.Text;
  private readonly slots: HeatSlot[] = [];

  constructor(scene: Phaser.Scene, bus: EventBus, opts: MeterHudOptions = {}) {
    const rowY = (i: number): number => i * ROW_H;
    this.root = scene.add
      .container(opts.x ?? 6, opts.y ?? 6)
      .setDepth(opts.depth ?? 1000)
      .setScrollFactor(0);

    this.root.add(
      scene.add
        .rectangle(-4, -4, BAR_X + BAR_W + 8, ROW_H * 4 + 6, UI.ink, 0.55)
        .setOrigin(0, 0)
        .setStrokeStyle(1, UI.panelLip, 0.4),
    );

    const label = (text: string, row: number): Phaser.GameObjects.Text =>
      scene.add.text(0, rowY(row), text, font(7, UI.paper)).setAlpha(0.55);
    this.root.add([label('VIBE', 0), label('AGGRO', 1), label('HYPE', 2), label('HEAT', 3)]);

    // --- Vibe: neon sign. Glow is just a fatter, fainter copy of the tube.
    const vibeMidY = rowY(0) + BAR_H / 2;
    this.root.add(
      scene.add.rectangle(BAR_X, vibeMidY, BAR_W, BAR_H, UI.ink, 0.7).setOrigin(0, 0.5),
    );
    this.vibeGlow = scene.add
      .rectangle(BAR_X, vibeMidY, BAR_W, BAR_H + 5, UI.neonGreen, 0.18)
      .setOrigin(0, 0.5);
    this.vibeFill = scene.add
      .rectangle(BAR_X, vibeMidY, BAR_W, BAR_H - 2, UI.neonGreen)
      .setOrigin(0, 0.5);
    this.root.add([this.vibeGlow, this.vibeFill]);

    // --- Aggro: segmented crowd-temperature strip, cool on the left.
    this.aggroStrip = scene.add.container(BAR_X, rowY(1) + BAR_H / 2);
    for (let i = 0; i < SEGMENTS; i++) {
      const seg = scene.add.rectangle(
        i * (SEG_W + SEG_GAP) + SEG_W / 2,
        0,
        SEG_W,
        BAR_H,
        mix(AGGRO_COOL, UI.danger, i / (SEGMENTS - 1)),
      );
      this.segs.push(seg);
      this.aggroStrip.add(seg);
    }
    this.root.add(this.aggroStrip);

    // --- Hype: a bonus badge, not a bar. It only ever multiplies good news.
    this.hypeBadge = scene.add.container(BAR_X + 14, rowY(2) + BAR_H / 2);
    this.hypeText = scene.add.text(0, 0, 'x1.0', font(8, UI.kebabAmber)).setOrigin(0.5);
    this.hypeBadge.add([
      scene.add
        .rectangle(0, 0, 28, 10, UI.kebabAmber, 0.16)
        .setStrokeStyle(1, UI.kebabAmber, 0.7),
      this.hypeText,
    ]);
    this.root.add(this.hypeBadge);

    // --- Heat: three licence-stamp slots. The third is the one that shuts you.
    for (let i = 0; i < HEAT_SLOTS; i++) {
      const final = i === HEAT_SLOTS - 1;
      const cx = BAR_X + i * (SLOT + SLOT_GAP) + SLOT / 2;
      const cy = rowY(3) + BAR_H / 2;
      this.root.add(
        scene.add
          .rectangle(cx, cy, SLOT, SLOT, UI.ink, 0.35)
          .setStrokeStyle(1, UI.paper, final ? 0.55 : 0.3),
      );

      const mark = scene.add.container(cx, cy).setVisible(false);
      const crossColour = final ? UI.paper : UI.danger;
      mark.add(scene.add.rectangle(0, 0, SLOT + 2, SLOT + 2, UI.danger, final ? 0.95 : 0.3));
      for (const angle of [45, -45]) {
        mark.add(
          scene.add
            .rectangle(0, 0, SLOT + 4, final ? 3 : 2, crossColour)
            .setAngle(angle)
            .setAlpha(final ? 1 : 0.9),
        );
      }
      if (final) {
        mark.add(
          scene.add.rectangle(0, 0, SLOT + 3, SLOT + 3, 0x000000, 0).setStrokeStyle(2, UI.danger),
        );
        mark.setAngle(-8);
      }
      this.root.add(mark);
      this.slots.push({ mark, scale: 1 });
    }

    for (let i = 0; i < Math.min(opts.initialHeat ?? 0, HEAT_SLOTS); i++) {
      this.strikes++;
      this.slots[i]?.mark.setVisible(true);
    }

    this.offs.push(
      bus.on('meters:changed', ({ vibe, aggro, hype }) => {
        this.vibe = vibe;
        this.aggro = aggro;
        if (hype > this.hype) this.hypeScale = 1.7;
        this.hype = hype;
        this.hypeText.setText(`x${hype.toFixed(1)}`);
      }),
      bus.on('heat:strike', () => {
        const slot = this.slots[this.strikes];
        this.strikes++;
        if (!slot) return;
        slot.scale = 2.4;
        slot.mark.setVisible(true);
      }),
    );

    this.redraw();
  }

  update(deltaMs: number): void {
    this.vibeShown = approach(this.vibeShown, this.vibe, deltaMs, 90);
    this.aggroShown = approach(this.aggroShown, this.aggro, deltaMs, 110);
    this.hypeScale = approach(this.hypeScale, 1, deltaMs, 80);

    const dying = clamp01((VIBE_FLICKER_BELOW - this.vibeShown) / VIBE_FLICKER_BELOW);
    this.flickerPhase += deltaMs * 0.006 * (1 + dying * 3);
    const hot = clamp01((this.aggroShown - AGGRO_PULSE_ABOVE) / (100 - AGGRO_PULSE_ABOVE));
    this.pulsePhase += deltaMs * 0.004 * (1 + hot * 2.5);

    for (const slot of this.slots) {
      slot.scale = approach(slot.scale, 1, deltaMs, 70);
      slot.mark.setScale(slot.scale);
    }

    this.redraw();
  }

  private redraw(): void {
    const vibeT = clamp01(this.vibeShown / 100);
    const colour = mix(UI.neonPink, UI.neonGreen, vibeT);
    const lit = Math.max(0.001, BAR_W * vibeT);
    this.vibeFill.setSize(lit, BAR_H - 2).setFillStyle(colour);
    this.vibeGlow.setSize(lit + 4, BAR_H + 5).setFillStyle(colour);

    const dying = clamp01((VIBE_FLICKER_BELOW - this.vibeShown) / VIBE_FLICKER_BELOW);
    const a = this.neonAlpha(dying);
    this.vibeFill.setAlpha(a);
    this.vibeGlow.setAlpha(0.18 * a);

    const litSegs = (this.aggroShown / 100) * SEGMENTS;
    const hot = clamp01((this.aggroShown - AGGRO_PULSE_ABOVE) / (100 - AGGRO_PULSE_ABOVE));
    const breath = 0.5 + 0.5 * Math.sin(this.pulsePhase);
    const pulse = 1 - 0.4 * hot * breath;
    for (let i = 0; i < this.segs.length; i++) {
      const seg = this.segs[i];
      if (!seg) continue;
      const on = clamp01(litSegs - i);
      seg.setAlpha(on > 0 ? (0.35 + 0.65 * on) * pulse : 0.14);
    }
    this.aggroStrip.setScale(1, 1 + 0.28 * hot * breath);

    this.hypeBadge.setScale(this.hypeScale);
  }

  /**
   * Three sines at incommensurate rates stand in for noise; the threshold rises
   * with `dying`, so blackouts get more frequent as the sign gives up.
   */
  private neonAlpha(dying: number): number {
    if (dying <= 0) return 1;
    const p = this.flickerPhase;
    const n = 0.5 * Math.sin(p) + 0.3 * Math.sin(p * 2.37 + 1.1) + 0.2 * Math.sin(p * 5.13 + 2.7);
    if (n < -0.8 + dying * 1.1) return 0.12;
    return 1 - 0.15 * dying * (0.5 + 0.5 * Math.sin(p * 3.1));
  }

  destroy(): void {
    for (const off of this.offs) off();
    this.offs.length = 0;
    this.root.destroy();
  }
}
