import { createLevel } from './level';
import { HEIGHT, WIDTH } from './types';
import type { Boss, Enemy, GameInput, GameState, Player, Rect, SoundEvent } from './types';

export const PHYSICS = {
  runSpeed: 128, acceleration: 1550, friction: 1750, gravity: 850,
  jumpSpeed: 330, terminalVelocity: 470, dashSpeed: 310, dashDuration: 0.18,
  wallSlideSpeed: 72, coyoteTime: 0.1, jumpBufferTime: 0.1,
};
const clamp = (n: number, min: number, max: number) => Math.max(min, Math.min(max, n));
const approach = (value: number, target: number, by: number) => value < target ? Math.min(value + by, target) : Math.max(value - by, target);
export const overlaps = (a: Rect, b: Rect) => a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
const centerX = (r: Rect) => r.x + r.w / 2;
const centerY = (r: Rect) => r.y + r.h / 2;
function sound(s: GameState, event: SoundEvent) { if (s.events.length < 64) s.events.push(event); }

function makePlayer(x: number, y: number): Player {
  return { x, y, w: 16, h: 26, vx: 0, vy: 0, facing: 1,
    hp: 28, maxHp: 28, grounded: false, wall: 0, charge: 0, dashTime: 0,
    dashCooldown: 0, airDashUsed: false, invulnerable: 0, coyote: 0,
    jumpBuffer: 0, shootCooldown: 0, wallLock: 0 };
}
function makeBoss(x: number): Boss {
  return { x, y: 312, w: 52, h: 48, vx: 0, vy: 0, hp: 120, maxHp: 120,
    active: false, phase: 'idle', timer: 1.6, cycle: 0, facing: -1,
    flash: 0, targetX: x };
}

export function createGame(): GameState {
  const level = createLevel();
  return {
    mode: 'title', elapsed: 0, deaths: 0, kills: 0, checkpoint: 0,
    player: makePlayer(64, 334), enemies: level.enemies.map(e => ({ ...e })),
    shots: [], particles: [], pickups: [], boss: makeBoss(level.bossX), level,
    camera: { x: 0, y: 191, shake: 0 }, notice: '', noticeTime: 0,
    events: [], frame: 0, nextId: 100,
  };
}

export function startGame(s: GameState): void {
  Object.assign(s, createGame());
  s.mode = 'playing';
  s.notice = 'MISSION START · HIGHWAY ASSAULT';
  s.noticeTime = 3;
}

/** A retry restores the entire encounter, retaining the run timer and death count. */
export function retryGame(s: GameState): void {
  const checkpoint = s.level.checkpoints[s.checkpoint];
  s.player = makePlayer(checkpoint.x, checkpoint.y);
  s.player.invulnerable = 1.25;
  s.enemies = s.level.enemies.map(e => ({ ...e }));
  s.boss = makeBoss(s.level.bossX);
  s.shots = [];
  s.particles = [];
  s.pickups = [];
  s.events = [];
  s.mode = 'playing';
  s.nextId = 100;
  s.camera.x = clamp(checkpoint.x - 90, 0, s.level.width - WIDTH);
  s.camera.y = clamp(checkpoint.y - HEIGHT * 0.62, 0, s.level.height - HEIGHT);
  s.camera.shake = 0;
  s.notice = `REDEPLOYED · ${checkpoint.label}`;
  s.noticeTime = 2.8;
}

function burst(s: GameState, x: number, y: number, count: number, color: string, force = 75): void {
  for (let i = 0; i < count; i++) {
    const angle = (i / count) * Math.PI * 2 + s.frame * 0.73;
    const speed = force * (0.35 + ((i * 37 + s.frame * 11) % 67) / 100);
    const life = 0.18 + ((i * 13) % 30) / 100;
    s.particles.push({ x, y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed,
      life, maxLife: life, color: i % 3 === 0 ? '#fff4cf' : color, size: i % 3 + 1 });
  }
  if (s.particles.length > 260) s.particles.splice(0, s.particles.length - 260);
}

export function damagePlayer(s: GameState, damage: number, fromX: number): boolean {
  const p = s.player;
  if (p.invulnerable > 0 || s.mode !== 'playing') return false;
  p.hp = Math.max(0, p.hp - damage);
  p.invulnerable = 1.1;
  p.dashTime = 0;
  p.charge = 0;
  p.vx = centerX(p) < fromX ? -130 : 130;
  p.vy = -155;
  p.grounded = false;
  p.wallLock = 0.18;
  s.camera.shake = 4;
  burst(s, centerX(p), centerY(p), 9, '#56caff');
  sound(s, 'hit');
  if (p.hp <= 0) killPlayer(s);
  return true;
}

function killPlayer(s: GameState): void {
  if (s.mode !== 'playing') return;
  s.player.hp = 0;
  s.player.charge = 0;
  s.player.dashTime = 0;
  s.player.vx = 0;
  s.player.vy = 0;
  s.mode = 'dead';
  s.deaths++;
  burst(s, centerX(s.player), centerY(s.player), 32, '#44c7ff', 150);
  sound(s, 'death');
}

/** Swept axis separation resolves solid roads, platforms, and stair walls. */
function movePlayer(s: GameState, dt: number): void {
  const p = s.player;
  p.wall = 0;
  p.x += p.vx * dt;
  for (const platform of s.level.platforms) {
    if (!overlaps(p, platform)) continue;
    if (p.vx > 0) { p.x = platform.x - p.w; p.wall = 1; }
    else if (p.vx < 0) { p.x = platform.x + platform.w; p.wall = -1; }
    p.vx = 0;
    p.dashTime = 0;
  }
  p.x = clamp(p.x, s.boss.active ? s.level.arena.x : 0, s.level.width - p.w);
  const previousBottom = p.y + p.h;
  p.y += p.vy * dt;
  p.grounded = false;
  for (const platform of s.level.platforms) {
    if (!overlaps(p, platform)) continue;
    if (p.vy >= 0 && previousBottom <= platform.y + 1) {
      p.y = platform.y - p.h;
      p.vy = 0;
      p.grounded = true;
      p.airDashUsed = false;
    } else if (p.vy < 0) {
      p.y = platform.y + platform.h;
      p.vy = 0;
    }
  }
  // Preserve wall contact while resting flush against a surface.
  if (!p.grounded) {
    const inset = { x: p.x - 1, y: p.y + 2, w: p.w + 2, h: p.h - 4 };
    for (const platform of s.level.platforms) {
      if (!overlaps(inset, platform)) continue;
      if (p.x + p.w <= platform.x + 1) p.wall = 1;
      else if (p.x >= platform.x + platform.w - 1) p.wall = -1;
    }
  }
}

function firePlayer(s: GameState, charged: number): void {
  const p = s.player;
  if (!charged && p.shootCooldown > 0) return;
  const w = charged === 2 ? 25 : charged === 1 ? 16 : 9;
  const h = charged === 2 ? 17 : charged === 1 ? 10 : 5;
  s.shots.push({ id: s.nextId++, x: p.facing > 0 ? p.x + p.w - 2 : p.x - w + 2,
    y: p.y + 11 - h / 2, w, h, vx: p.facing * (charged === 2 ? 430 : 390), vy: 0,
    damage: charged === 2 ? 8 : charged === 1 ? 4 : 1, charged, owner: 'player', life: 1.65 });
  p.shootCooldown = 0.18;
  burst(s, p.facing > 0 ? p.x + p.w + 2 : p.x - 2, p.y + 11, charged ? 7 : 3, charged ? '#7effa4' : '#ffeaa0', 30);
  sound(s, charged ? 'charged-shot' : 'shot');
}

function updatePlayer(s: GameState, input: GameInput, dt: number): void {
  const p = s.player;
  p.invulnerable = Math.max(0, p.invulnerable - dt);
  p.shootCooldown = Math.max(0, p.shootCooldown - dt);
  p.dashCooldown = Math.max(0, p.dashCooldown - dt);
  p.wallLock = Math.max(0, p.wallLock - dt);
  p.coyote = p.grounded ? PHYSICS.coyoteTime : Math.max(0, p.coyote - dt);
  p.jumpBuffer = input.jumpPressed ? PHYSICS.jumpBufferTime : Math.max(0, p.jumpBuffer - dt);

  const direction = (input.right ? 1 : 0) - (input.left ? 1 : 0);
  if (direction && p.wallLock <= 0 && p.dashTime <= 0) p.facing = direction > 0 ? 1 : -1;
  if (p.jumpBuffer > 0 && (p.grounded || p.coyote > 0 || p.wall !== 0)) {
    if (!p.grounded && p.coyote <= 0 && p.wall !== 0) {
      p.vx = -p.wall * 208;
      p.facing = p.wall > 0 ? -1 : 1;
      p.wallLock = 0.17;
      burst(s, centerX(p), p.y + p.h, 5, '#b2ecff', 35);
    }
    p.vy = -PHYSICS.jumpSpeed;
    p.grounded = false;
    p.coyote = 0;
    p.jumpBuffer = 0;
    p.dashTime = 0;
    sound(s, 'jump');
  }
  if (input.jumpReleased && p.vy < -125) p.vy = -125;

  if (input.dashPressed && p.dashCooldown <= 0 && (p.grounded || !p.airDashUsed)) {
    if (!p.grounded) p.airDashUsed = true;
    p.dashTime = PHYSICS.dashDuration;
    p.dashCooldown = 0.34;
    p.wallLock = 0;
    p.vy = 0;
    sound(s, 'dash');
    burst(s, centerX(p), p.y + p.h - 2, 7, '#97d9ff', 45);
  }
  if (p.dashTime > 0) {
    p.dashTime = Math.max(0, p.dashTime - dt);
    p.vx = p.facing * PHYSICS.dashSpeed;
    p.vy = p.grounded ? 20 : 0;
    if (s.frame % 3 === 0) burst(s, centerX(p) - p.facing * 9, p.y + p.h - 3, 2, '#5ed8ff', 22);
  } else {
    if (p.wallLock <= 0) {
      p.vx = approach(p.vx, direction * PHYSICS.runSpeed, (direction ? PHYSICS.acceleration : PHYSICS.friction) * dt);
    }
    p.vy = Math.min(PHYSICS.terminalVelocity, p.vy + PHYSICS.gravity * dt);
    if (p.wall && direction === p.wall && p.vy > PHYSICS.wallSlideSpeed && p.wallLock <= 0) p.vy = PHYSICS.wallSlideSpeed;
  }
  movePlayer(s, dt);

  if (input.shootPressed) { firePlayer(s, 0); p.charge = 0; }
  if (input.shoot) {
    const oldCharge = p.charge;
    p.charge = Math.min(1.8, p.charge + dt);
    if (oldCharge < 0.6 && p.charge >= 0.6) sound(s, 'charge');
    if (oldCharge < 1.2 && p.charge >= 1.2) sound(s, 'charge');
  }
  if (input.shootReleased) {
    if (p.charge >= 0.6) firePlayer(s, p.charge >= 1.2 ? 2 : 1);
    p.charge = 0;
  }
  if (!input.shoot && !input.shootReleased) p.charge = 0;

  if (p.y > 485) killPlayer(s);
  for (const hazard of s.level.hazards) if (overlaps(p, hazard)) damagePlayer(s, 6, centerX(hazard));
}

function enemyShot(s: GameState, x: number, y: number, vx: number, vy: number, damage = 3, size = 6): void {
  s.shots.push({ id: s.nextId++, x: x - size / 2, y: y - size / 2, w: size, h: size,
    vx, vy, damage, charged: 0, owner: 'enemy', life: 4.2 });
}
function aimedShot(s: GameState, enemy: Rect, speed: number): void {
  const dx = centerX(s.player) - centerX(enemy);
  const dy = centerY(s.player) - centerY(enemy);
  const length = Math.max(1, Math.hypot(dx, dy));
  enemyShot(s, centerX(enemy), centerY(enemy) - 2, dx / length * speed, dy / length * speed);
}
function destroyEnemy(s: GameState, enemy: Enemy): void {
  s.kills++;
  burst(s, centerX(enemy), centerY(enemy), 18, '#ff9445', 95);
  sound(s, 'explosion');
  // Fixed drop pattern makes replays and physics tests reproducible.
  if (enemy.id % 3 === 0 || s.player.hp <= 12) {
    const below = s.level.platforms.filter(p => centerX(enemy) >= p.x && centerX(enemy) <= p.x + p.w && p.y >= enemy.y);
    const floor = below.sort((a, b) => a.y - b.y)[0];
    s.pickups.push({ kind: 'health', x: centerX(enemy) - 5, y: floor ? floor.y - 12 : enemy.y, w: 10, h: 12, life: 24 });
  }
}

function updateEnemies(s: GameState, dt: number): void {
  for (const enemy of s.enemies) {
    enemy.flash = Math.max(0, enemy.flash - dt);
    if (enemy.hp <= 0 || Math.abs(centerX(enemy) - centerX(s.player)) > 420) continue;
    enemy.timer -= dt;
    const dx = centerX(s.player) - centerX(enemy);
    if (enemy.kind === 'walker') {
      enemy.vx = enemy.facing * 25;
      const nextX = enemy.x + enemy.vx * dt;
      const footX = enemy.facing > 0 ? nextX + enemy.w + 2 : nextX - 2;
      const support = s.level.platforms.some(p => footX >= p.x && footX <= p.x + p.w && Math.abs(p.y - (enemy.y + enemy.h)) < 3);
      if (Math.abs(nextX - enemy.originX) > 65 || !support) enemy.facing = enemy.facing === 1 ? -1 : 1;
      else enemy.x = nextX;
      if (enemy.timer <= 0 && Math.abs(dx) < 245 && Math.abs(centerY(s.player) - centerY(enemy)) < 95) {
        aimedShot(s, enemy, 104);
        enemy.timer = 2.5;
      }
    } else if (enemy.kind === 'drone') {
      enemy.phase += dt;
      enemy.x = enemy.originX + Math.sin(enemy.phase * 1.5) * 36;
      enemy.y = enemy.originY + Math.sin(enemy.phase * 2.3) * 15;
      enemy.facing = dx >= 0 ? 1 : -1;
      if (enemy.timer <= 0 && Math.abs(dx) < 270) { aimedShot(s, enemy, 115); enemy.timer = 2.2; }
    } else {
      enemy.facing = dx >= 0 ? 1 : -1;
      if (enemy.timer <= 0 && Math.abs(dx) < 290) { aimedShot(s, enemy, 130); enemy.timer = 1.85; }
    }
    if (overlaps(s.player, enemy)) damagePlayer(s, 3, centerX(enemy));
  }
}

function updateBoss(s: GameState, dt: number): void {
  const b = s.boss;
  b.flash = Math.max(0, b.flash - dt);
  if (b.hp <= 0) return;
  if (!b.active) {
    if (s.player.x > s.level.arena.x + 25) {
      b.active = true;
      s.camera.x = s.level.arena.x;
      b.timer = 1.8;
      s.enemies = [];
      s.shots = s.shots.filter(shot => shot.owner === 'player');
      s.notice = 'WARNING · IRON COLOSSUS';
      s.noticeTime = 3;
      sound(s, 'boss');
    }
    return;
  }
  b.timer -= dt;
  const enraged = b.hp <= b.maxHp / 2;
  const speed = enraged ? 1.24 : 1;
  if (b.phase === 'idle' && b.timer <= 0) {
    b.facing = centerX(s.player) < centerX(b) ? -1 : 1;
    b.phase = ['windup-dash', 'windup-shot', 'windup-slam'][b.cycle % 3] as Boss['phase'];
    b.cycle++;
    b.timer = (b.phase === 'windup-slam' ? 0.95 : 0.8) / speed;
    b.targetX = clamp(s.player.x, s.level.arena.x + 20, s.level.width - b.w - 20);
    b.vx = 0;
  } else if (b.phase === 'windup-dash' && b.timer <= 0) {
    b.phase = 'dash'; b.timer = 0.95; b.vx = b.facing * 235 * speed;
    sound(s, 'dash');
  } else if (b.phase === 'dash') {
    b.x += b.vx * dt;
    if (s.frame % 4 === 0) burst(s, centerX(b), b.y + b.h - 3, 3, '#ff9b43', 55);
    if (b.timer <= 0 || b.x <= s.level.arena.x + 10 || b.x + b.w >= s.level.width - 10) {
      b.x = clamp(b.x, s.level.arena.x + 10, s.level.width - b.w - 10);
      b.phase = 'recover'; b.timer = 0.8 / speed; b.vx = 0;
    }
  } else if (b.phase === 'windup-shot' && b.timer <= 0) {
    b.phase = 'shoot'; b.timer = 1.0;
    const dx = centerX(s.player) - centerX(b);
    const dy = centerY(s.player) - (b.y + 24);
    const angle = Math.atan2(dy, dx);
    const count = enraged ? 5 : 3;
    for (let i = 0; i < count; i++) {
      const a = angle + (i - (count - 1) / 2) * 0.19;
      enemyShot(s, centerX(b) + b.facing * 29, b.y + 24, Math.cos(a) * 128 * speed, Math.sin(a) * 128 * speed, 4, 8);
    }
    sound(s, 'shot');
  } else if (b.phase === 'shoot' && b.timer <= 0) {
    b.phase = 'recover'; b.timer = 0.85 / speed;
  } else if (b.phase === 'windup-slam' && b.timer <= 0) {
    b.phase = 'slam'; b.timer = 1.4;
    b.vy = -330;
    b.vx = (b.targetX - b.x) / 0.78;
    sound(s, 'jump');
  } else if (b.phase === 'slam') {
    b.vy += PHYSICS.gravity * dt;
    b.x = clamp(b.x + b.vx * dt, s.level.arena.x + 10, s.level.width - b.w - 10);
    b.y += b.vy * dt;
    if (b.y >= 360 - b.h && b.vy > 0) {
      b.y = 360 - b.h; b.vy = 0; b.vx = 0;
      b.phase = 'recover'; b.timer = 1.15 / speed;
      s.camera.shake = 6;
      burst(s, centerX(b), 356, 28, '#ff9144', 135);
      enemyShot(s, b.x - 8, 352, -145 * speed, 0, 4, 12);
      enemyShot(s, b.x + b.w + 8, 352, 145 * speed, 0, 4, 12);
      sound(s, 'explosion');
    }
  } else if (b.phase === 'recover' && b.timer <= 0) {
    b.phase = 'idle'; b.timer = 0.7 / speed;
  }
  if (overlaps(s.player, b)) damagePlayer(s, b.phase === 'dash' || b.phase === 'slam' ? 5 : 3, centerX(b));
}

function updateShots(s: GameState, dt: number): void {
  for (const shot of s.shots) {
    shot.x += shot.vx * dt;
    shot.y += shot.vy * dt;
    shot.life -= dt;
    if (shot.life <= 0) continue;
    if (shot.owner === 'player') {
      for (const enemy of s.enemies) {
        if (enemy.hp <= 0 || !overlaps(shot, enemy)) continue;
        enemy.hp -= shot.damage;
        enemy.flash = 0.1;
        shot.life = 0;
        burst(s, centerX(shot), centerY(shot), 5, '#fff1ac', 50);
        if (enemy.hp <= 0) destroyEnemy(s, enemy);
        break;
      }
      if (shot.life > 0 && s.boss.active && s.boss.hp > 0 && overlaps(shot, s.boss)) {
        s.boss.hp = Math.max(0, s.boss.hp - shot.damage);
        s.boss.flash = 0.1;
        shot.life = 0;
        burst(s, centerX(shot), centerY(shot), 9, '#ffd05e', 70);
        if (s.boss.hp === 0) {
          s.boss.phase = 'defeated';
          s.boss.vx = 0; s.boss.vy = 0;
          s.mode = 'won';
          s.player.charge = 0;
          s.player.dashTime = 0;
          s.player.vx = 0; s.player.vy = 0;
          s.notice = 'MISSION COMPLETE';
          s.noticeTime = 10;
          s.camera.shake = 8;
          burst(s, centerX(s.boss), centerY(s.boss), 65, '#ffa64b', 180);
          sound(s, 'win');
        }
      }
    } else if (overlaps(shot, s.player)) {
      damagePlayer(s, shot.damage, centerX(shot));
      shot.life = 0;
    }
    if (shot.life > 0 && s.level.platforms.some(p => overlaps(shot, p))) {
      shot.life = 0;
      burst(s, centerX(shot), centerY(shot), 3, '#ffe4a3', 25);
    }
  }
  s.shots = s.shots.filter(shot => shot.life > 0 && shot.x > s.camera.x - 300 && shot.x < s.camera.x + WIDTH + 450);
  s.enemies = s.enemies.filter(e => e.hp > 0);
  if (s.mode === 'won') s.shots = [];
}

function updateAmbient(s: GameState, dt: number): void {
  for (const particle of s.particles) {
    particle.life -= dt;
    particle.x += particle.vx * dt;
    particle.y += particle.vy * dt;
    particle.vy += 110 * dt;
  }
  s.particles = s.particles.filter(p => p.life > 0);
  s.camera.shake = Math.max(0, s.camera.shake - 20 * dt);
  s.noticeTime = Math.max(0, s.noticeTime - dt);
}

function updateCheckpoints(s: GameState): void {
  for (let i = s.checkpoint + 1; i < s.level.checkpoints.length; i++) {
    if (s.player.x < s.level.checkpoints[i].x || s.player.y > 400) continue;
    s.checkpoint = i;
    s.player.hp = s.player.maxHp;
    s.notice = `CHECKPOINT ${String(i).padStart(2, '0')} · ENERGY RESTORED`;
    s.noticeTime = 3;
    burst(s, centerX(s.player), centerY(s.player), 18, '#5ffff2', 60);
    sound(s, 'checkpoint');
  }
}

export function stepGame(s: GameState, input: GameInput, dt = 1 / 60): void {
  dt = clamp(dt, 0, 1 / 30);
  if (s.mode === 'paused' || s.mode === 'title') return;
  updateAmbient(s, dt);
  if (s.mode !== 'playing') return;
  s.elapsed += dt;
  s.frame++;
  updatePlayer(s, input, dt);
  if (s.mode !== 'playing') return;
  updateCheckpoints(s);
  updateEnemies(s, dt);
  if (s.mode !== 'playing') return;
  updateBoss(s, dt);
  if (s.mode !== 'playing') return;
  updateShots(s, dt);
  if (s.mode !== 'playing') return;
  for (const pickup of s.pickups) {
    pickup.life -= dt;
    if (overlaps(s.player, pickup) && s.player.hp < s.player.maxHp) {
      s.player.hp = Math.min(s.player.maxHp, s.player.hp + 7);
      pickup.life = 0;
      sound(s, 'pickup');
      burst(s, centerX(pickup), centerY(pickup), 10, '#62ffbb', 45);
    }
  }
  s.pickups = s.pickups.filter(p => p.life > 0);
  const cameraX = s.boss.active ? s.level.arena.x : clamp(centerX(s.player) - WIDTH * 0.38, 0, s.level.width - WIDTH);
  const cameraY = clamp(centerY(s.player) - HEIGHT * 0.62, 0, s.level.height - HEIGHT);
  const smooth = 1 - Math.exp(-9 * dt);
  s.camera.x += (cameraX - s.camera.x) * smooth;
  s.camera.y += (cameraY - s.camera.y) * smooth;
}
