/**
 * Simulation: player movement + collision, sliding doors, enemy AI
 * (idle → alert → chase → melee/projectile), hitscan gun, pickups, exit.
 */
import { CELL, buildLevel, type Level, type EnemyKind, type PickupKind } from './level';
import type { Sfx } from './audio';
import type { Input } from './input';

export type Phase = 'title' | 'playing' | 'dead' | 'won';

export interface Door {
  x: number;
  y: number;
  open: number; // 0 closed .. 1 open
  state: 'closed' | 'opening' | 'open' | 'closing';
  timer: number;
  locked: boolean;
}

export interface Enemy {
  kind: EnemyKind;
  x: number;
  y: number;
  hp: number;
  maxHp: number;
  state: 'idle' | 'chase' | 'attack' | 'pain' | 'dead';
  attackT: number;
  cooldown: number;
  flash: number;
  pain: number;
  deathT: number;
  bobSeed: number;
  growlCd: number;
  radius: number;
  worldH: number;
}

export interface Pickup {
  kind: PickupKind;
  x: number;
  y: number;
  taken: boolean;
}

export interface Projectile {
  x: number;
  y: number;
  vx: number;
  vy: number;
  alive: boolean;
}

export interface Puff {
  x: number;
  y: number;
  z: number; // 0 floor .. 1 ceiling (sprite center height)
  t: number;
  max: number;
  kind: 'blood' | 'spark';
}

const ENEMY_DEF: Record<EnemyKind, { hp: number; speed: number; radius: number; worldH: number; melee: number; meleeDmg: number }> = {
  husk: { hp: 3, speed: 1.35, radius: 0.3, worldH: 0.72, melee: 1.15, meleeDmg: 8 },
  stalker: { hp: 3, speed: 2.3, radius: 0.28, worldH: 0.68, melee: 1.0, meleeDmg: 6 },
  brute: { hp: 12, speed: 1.05, radius: 0.4, worldH: 0.95, melee: 1.3, meleeDmg: 18 },
};

const PLAYER_RADIUS = 0.27;
const FIRE_COOLDOWN = 0.42;

export class Game {
  level: Level = buildLevel();
  phase: Phase = 'title';

  // player
  px = 0; py = 0; angle = 0;
  hp = 100;
  ammo = 24;
  hasKey = false;
  kills = 0;

  doors = new Map<number, Door>();
  enemies: Enemy[] = [];
  pickups: Pickup[] = [];
  projectiles: Projectile[] = [];
  puffs: Puff[] = [];

  // presentation state read by the renderer / hud
  muzzle = 0;       // 0..1 muzzle flash
  gunKick = 0;      // 0..1 recoil
  damageFlash = 0;  // 0..1 red vignette
  pickupFlash = 0;  // 0..1 white blink
  shake = 0;
  walkCycle = 0;
  moving = 0;       // 0..1 how much we are moving (gun sway)
  fireCd = 0;
  toast = '';
  toastT = 0;
  time = 0;
  private lockedMsgCd = 0;

  constructor(private sfx: Sfx) {
    this.reset();
  }

  reset(): void {
    this.level = buildLevel();
    const l = this.level;
    this.px = l.spawnX;
    this.py = l.spawnY;
    this.angle = l.spawnAngle;
    this.hp = 100;
    this.ammo = 24;
    this.hasKey = false;
    this.kills = 0;
    this.time = 0;
    this.muzzle = 0; this.gunKick = 0; this.damageFlash = 0; this.shake = 0;
    this.fireCd = 0; this.toast = ''; this.toastT = 0; this.pickupFlash = 0;
    this.doors.clear();
    for (let y = 0; y < l.h; y++) {
      for (let x = 0; x < l.w; x++) {
        const c = l.grid[y * l.w + x];
        if (c === CELL.DOOR || c === CELL.LOCKED) {
          this.doors.set(y * l.w + x, {
            x, y, open: 0, state: 'closed', timer: 0, locked: c === CELL.LOCKED,
          });
        }
      }
    }
    this.enemies = l.enemies.map((s) => {
      const d = ENEMY_DEF[s.kind];
      return {
        kind: s.kind, x: s.x, y: s.y, hp: d.hp, maxHp: d.hp,
        state: 'idle' as const, attackT: 0, cooldown: 0, flash: 0, pain: 0,
        deathT: 0, bobSeed: Math.random() * 7, growlCd: 0,
        radius: d.radius, worldH: d.worldH,
      };
    });
    this.pickups = l.pickups.map((p) => ({ ...p, taken: false }));
    this.projectiles = [];
    this.puffs = [];
  }

  aliveEnemies(): number {
    return this.enemies.reduce((n, e) => n + (e.state !== 'dead' ? 1 : 0), 0);
  }

  doorAt(x: number, y: number): Door | undefined {
    return this.doors.get(y * this.level.w + x);
  }

  /** is this cell solid for movement? */
  private solid(x: number, y: number): boolean {
    const l = this.level;
    if (x < 0 || y < 0 || x >= l.w || y >= l.h) return true;
    const c = l.grid[y * l.w + x];
    if (c === CELL.FLOOR) return false;
    if (c === CELL.DOOR || c === CELL.LOCKED) {
      const d = this.doors.get(y * l.w + x);
      return !d || d.open < 0.75;
    }
    return true;
  }

  /** circle-vs-grid: can an entity of radius r stand at (x, y)? */
  canStand(x: number, y: number, r: number): boolean {
    const x0 = Math.floor(x - r), x1 = Math.floor(x + r);
    const y0 = Math.floor(y - r), y1 = Math.floor(y + r);
    for (let cy = y0; cy <= y1; cy++) {
      for (let cx = x0; cx <= x1; cx++) {
        if (!this.solid(cx, cy)) continue;
        // closest point on cell to circle center
        const nx = Math.max(cx, Math.min(x, cx + 1));
        const ny = Math.max(cy, Math.min(y, cy + 1));
        if ((nx - x) ** 2 + (ny - y) ** 2 < r * r) return false;
      }
    }
    return true;
  }

  /** move with axis-separated wall sliding; returns actual movement length */
  private slide(e: { x: number; y: number }, dx: number, dy: number, r: number): number {
    const ox = e.x, oy = e.y;
    if (this.canStand(e.x + dx, e.y, r)) e.x += dx;
    if (this.canStand(e.x, e.y + dy, r)) e.y += dy;
    return Math.hypot(e.x - ox, e.y - oy);
  }

  /**
   * Grid raycast for LOS / hitscan. Returns distance to the first blocking
   * cell, or maxDist. Doors block while mostly closed.
   */
  castRay(x: number, y: number, dx: number, dy: number, maxDist: number): number {
    const l = this.level;
    let mapX = Math.floor(x), mapY = Math.floor(y);
    const deltaX = Math.abs(1 / (dx || 1e-9));
    const deltaY = Math.abs(1 / (dy || 1e-9));
    const stepX = dx < 0 ? -1 : 1;
    const stepY = dy < 0 ? -1 : 1;
    let sideX = dx < 0 ? (x - mapX) * deltaX : (mapX + 1 - x) * deltaX;
    let sideY = dy < 0 ? (y - mapY) * deltaY : (mapY + 1 - y) * deltaY;
    for (let i = 0; i < 64; i++) {
      let dist: number;
      if (sideX < sideY) {
        dist = sideX; sideX += deltaX; mapX += stepX;
      } else {
        dist = sideY; sideY += deltaY; mapY += stepY;
      }
      if (dist > maxDist) return maxDist;
      if (mapX < 0 || mapY < 0 || mapX >= l.w || mapY >= l.h) return dist;
      const c = l.grid[mapY * l.w + mapX];
      if (c === CELL.FLOOR) continue;
      if (c === CELL.DOOR || c === CELL.LOCKED) {
        const d = this.doors.get(mapY * l.w + mapX);
        if (d && d.open > 0.5) continue;
      }
      return dist + 0.01;
    }
    return maxDist;
  }

  hasLOS(x0: number, y0: number, x1: number, y1: number): boolean {
    const dx = x1 - x0, dy = y1 - y0;
    const dist = Math.hypot(dx, dy);
    if (dist < 0.001) return true;
    return this.castRay(x0, y0, dx / dist, dy / dist, dist) >= dist;
  }

  private setToast(msg: string): void {
    this.toast = msg;
    this.toastT = 2.2;
  }

  fire(): void {
    if (this.phase !== 'playing' || this.fireCd > 0) return;
    if (this.ammo <= 0) {
      this.fireCd = 0.3;
      this.sfx.click();
      this.setToast('OUT OF SHELLS');
      return;
    }
    this.ammo--;
    this.fireCd = FIRE_COOLDOWN;
    this.muzzle = 1;
    this.gunKick = 1;
    this.shake = Math.min(1, this.shake + 0.35);
    this.sfx.shoot();

    const dx = Math.cos(this.angle), dy = Math.sin(this.angle);
    const wallDist = this.castRay(this.px, this.py, dx, dy, 24);

    // pick the best enemy target near the ray
    let best: Enemy | null = null;
    let bestT = Infinity;
    for (const e of this.enemies) {
      if (e.state === 'dead') continue;
      const rx = e.x - this.px, ry = e.y - this.py;
      const t = rx * dx + ry * dy; // along ray
      if (t < 0.2 || t > wallDist + 0.3) continue;
      const lat = Math.abs(rx * dy - ry * dx); // perpendicular offset
      const hitR = e.radius + 0.18 + t * 0.025; // slight aim assist with range
      if (lat < hitR && t < bestT) {
        best = e;
        bestT = t;
      }
    }
    if (best) {
      this.hitEnemy(best, 1, bestT);
    } else {
      // spark on the wall
      this.puffs.push({
        x: this.px + dx * (wallDist - 0.06),
        y: this.py + dy * (wallDist - 0.06),
        z: 0.5, t: 0, max: 0.16, kind: 'spark',
      });
    }
    // gunshot wakes the deck
    for (const e of this.enemies) {
      if (e.state === 'idle' && Math.hypot(e.x - this.px, e.y - this.py) < 7) {
        this.alert(e);
      }
    }
  }

  private alert(e: Enemy): void {
    if (e.state !== 'idle') return;
    e.state = 'chase';
    if (e.growlCd <= 0) {
      this.sfx.growl(e.kind);
      e.growlCd = 3 + Math.random() * 3;
    }
  }

  private hitEnemy(e: Enemy, dmg: number, dist: number): void {
    e.hp -= dmg;
    e.flash = 1;
    this.alert(e);
    this.puffs.push({ x: e.x, y: e.y, z: 0.55, t: 0, max: 0.22, kind: 'blood' });
    if (e.hp <= 0) {
      e.state = 'dead';
      e.deathT = 0;
      this.kills++;
      this.sfx.enemyDie();
    } else if (Math.random() < 0.65) {
      e.state = 'pain';
      e.pain = 0.28;
    }
    void dist;
  }

  damagePlayer(dmg: number): void {
    if (this.phase !== 'playing') return;
    this.hp -= dmg;
    this.damageFlash = 1;
    this.shake = Math.min(1, this.shake + 0.5);
    this.sfx.hurt();
    if (this.hp <= 0) {
      this.hp = 0;
      this.phase = 'dead';
    }
  }

  teleport(x: number, y: number, angle?: number): void {
    this.px = x;
    this.py = y;
    if (angle !== undefined) this.angle = angle;
  }

  update(dt: number, input: Input): void {
    this.time += dt;
    // decay presentation timers even when not playing (death fade etc.)
    this.muzzle = Math.max(0, this.muzzle - dt * 6.5);
    this.gunKick = Math.max(0, this.gunKick - dt * 5);
    this.damageFlash = Math.max(0, this.damageFlash - dt * 1.8);
    this.pickupFlash = Math.max(0, this.pickupFlash - dt * 4);
    this.shake = Math.max(0, this.shake - dt * 3);
    this.toastT = Math.max(0, this.toastT - dt);
    this.fireCd = Math.max(0, this.fireCd - dt);
    for (const p of this.puffs) p.t += dt;
    this.puffs = this.puffs.filter((p) => p.t < p.max);

    if (this.phase !== 'playing') return;

    // --- player ---
    input.keyboardTurn(dt);
    this.angle += input.takeTurn();
    const st = input.state;
    const cos = Math.cos(this.angle), sin = Math.sin(this.angle);
    const fwd = st.moveY * 3.1;
    const strafe = st.moveX * 2.5;
    const dx = (cos * fwd - sin * strafe) * dt;
    const dy = (sin * fwd + cos * strafe) * dt;
    const me = { x: this.px, y: this.py };
    const dist = this.slide(me, dx, dy, PLAYER_RADIUS);
    this.px = me.x; this.py = me.y;
    this.walkCycle += dist * 7;
    const target = Math.min(1, Math.hypot(st.moveX, st.moveY));
    this.moving += (target - this.moving) * Math.min(1, dt * 8);

    if (st.firing) this.fire();

    // --- doors ---
    this.lockedMsgCd = Math.max(0, this.lockedMsgCd - dt);
    for (const d of this.doors.values()) {
      const cx = d.x + 0.5, cy = d.y + 0.5;
      const pNear = Math.hypot(this.px - cx, this.py - cy) < 1.4;
      let eNear = false;
      if (!d.locked) {
        for (const e of this.enemies) {
          if (e.state !== 'dead' && e.state !== 'idle' &&
              Math.hypot(e.x - cx, e.y - cy) < 1.0) { eNear = true; break; }
        }
      }
      if ((pNear || eNear) && (d.state === 'closed' || d.state === 'closing')) {
        if (d.locked && !this.hasKey) {
          if (pNear && this.lockedMsgCd <= 0) {
            this.lockedMsgCd = 1.6;
            this.setToast('LOCKED — RED KEYCARD REQUIRED');
            this.sfx.locked();
          }
        } else {
          if (d.locked) {
            d.locked = false;
            this.setToast('KEYCARD ACCEPTED');
            this.sfx.keycard();
          }
          d.state = 'opening';
          this.sfx.doorHiss();
        }
      }
      switch (d.state) {
        case 'opening':
          d.open += dt * 1.3;
          if (d.open >= 1) { d.open = 1; d.state = 'open'; d.timer = 3.5; }
          break;
        case 'open': {
          let anyNear = Math.hypot(this.px - cx, this.py - cy) < 1.0;
          if (!anyNear) {
            for (const e of this.enemies) {
              if (e.state !== 'dead' && Math.hypot(e.x - cx, e.y - cy) < 1.0) {
                anyNear = true;
                break;
              }
            }
          }
          d.timer -= dt;
          if (d.timer <= 0 && !anyNear) {
            d.state = 'closing';
            this.sfx.doorHiss();
          }
          break;
        }
        case 'closing':
          d.open -= dt * 1.3;
          if (d.open <= 0) { d.open = 0; d.state = 'closed'; }
          break;
        case 'closed':
          break;
      }
    }

    // --- enemies ---
    for (const e of this.enemies) {
      if (e.state === 'dead') {
        e.deathT += dt;
        continue;
      }
      e.flash = Math.max(0, e.flash - dt * 6);
      e.growlCd = Math.max(0, e.growlCd - dt);
      e.cooldown = Math.max(0, e.cooldown - dt);
      const def = ENEMY_DEF[e.kind];
      const pdx = this.px - e.x, pdy = this.py - e.y;
      const pdist = Math.hypot(pdx, pdy);

      if (e.state === 'idle') {
        if (pdist < 9 && this.hasLOS(e.x, e.y, this.px, this.py)) this.alert(e);
        continue;
      }
      if (e.state === 'pain') {
        e.pain -= dt;
        if (e.pain <= 0) e.state = 'chase';
        continue;
      }
      if (e.state === 'attack') {
        e.attackT -= dt;
        if (e.attackT <= 0) {
          e.state = 'chase';
          if (e.kind === 'stalker' && pdist > def.melee) {
            // ranged volley
            if (this.hasLOS(e.x, e.y, this.px, this.py)) {
              const inv = 1 / (pdist || 1);
              this.projectiles.push({
                x: e.x + pdx * inv * 0.4, y: e.y + pdy * inv * 0.4,
                vx: pdx * inv * 4.2, vy: pdy * inv * 4.2, alive: true,
              });
              this.sfx.projectile();
            }
          } else if (pdist < def.melee + 0.35) {
            this.damagePlayer(def.meleeDmg);
          }
          e.cooldown = e.kind === 'stalker' ? 1.7 : 1.1;
        }
        continue;
      }

      // chase
      if (e.growlCd <= 0 && pdist < 8) {
        if (Math.random() < dt * 0.25) {
          this.sfx.growl(e.kind);
          e.growlCd = 4 + Math.random() * 4;
        }
      }
      const inv = 1 / (pdist || 1);
      let mx = pdx * inv, my = pdy * inv;
      // light separation from other enemies
      for (const o of this.enemies) {
        if (o === e || o.state === 'dead') continue;
        const sx = e.x - o.x, sy = e.y - o.y;
        const sd = Math.hypot(sx, sy);
        if (sd > 0.01 && sd < e.radius + o.radius + 0.2) {
          mx += (sx / sd) * 0.8;
          my += (sy / sd) * 0.8;
        }
      }
      const ml = Math.hypot(mx, my) || 1;
      const stopRange = e.kind === 'stalker' && this.hasLOS(e.x, e.y, this.px, this.py)
        ? 2.6 : def.melee * 0.8;
      if (pdist > stopRange) {
        const sp = def.speed * dt;
        const movedLen = this.slide(e, (mx / ml) * sp, (my / ml) * sp, e.radius);
        if (movedLen < sp * 0.25) {
          // stuck on a corner: nudge perpendicular
          const side = ((e.bobSeed * 13) | 0) % 2 === 0 ? 1 : -1;
          this.slide(e, (-my / ml) * sp * side, (mx / ml) * sp * side, e.radius);
        }
      }
      // start an attack?
      if (e.cooldown <= 0) {
        if (pdist < def.melee) {
          e.state = 'attack';
          e.attackT = 0.4;
        } else if (e.kind === 'stalker' && pdist < 8 &&
                   this.hasLOS(e.x, e.y, this.px, this.py)) {
          e.state = 'attack';
          e.attackT = 0.35;
        }
      }
    }

    // --- projectiles ---
    for (const pr of this.projectiles) {
      if (!pr.alive) continue;
      const nx = pr.x + pr.vx * dt;
      const ny = pr.y + pr.vy * dt;
      if (!this.canStand(nx, ny, 0.12)) {
        pr.alive = false;
        this.puffs.push({ x: pr.x, y: pr.y, z: 0.5, t: 0, max: 0.2, kind: 'spark' });
        continue;
      }
      pr.x = nx; pr.y = ny;
      if (Math.hypot(pr.x - this.px, pr.y - this.py) < 0.38) {
        pr.alive = false;
        this.damagePlayer(10);
      }
    }
    this.projectiles = this.projectiles.filter((p) => p.alive);

    // --- pickups ---
    for (const p of this.pickups) {
      if (p.taken) continue;
      if (Math.hypot(p.x - this.px, p.y - this.py) < 0.55) {
        if (p.kind === 'medkit') {
          if (this.hp >= 100) continue; // leave it for later
          this.hp = Math.min(100, this.hp + 25);
          this.setToast('+25 HP');
          this.sfx.pickup();
        } else if (p.kind === 'ammo') {
          this.ammo += 8;
          this.setToast('+8 SHELLS');
          this.sfx.pickup();
        } else {
          this.hasKey = true;
          this.setToast('RED KEYCARD ACQUIRED');
          this.sfx.keycard();
        }
        p.taken = true;
        this.pickupFlash = 0.7;
      }
    }

    // --- exit ---
    if (Math.hypot(this.level.exitX - this.px, this.level.exitY - this.py) < 0.7) {
      this.phase = 'won';
      this.sfx.win();
    }
  }
}
