import { HEIGHT, WIDTH } from './types';
import type { Boss, Enemy, GameState, Platform, Player } from './types';

// All artwork is drawn on the logical pixel grid. No downloaded sprites or fonts.
const C = {
  sky: '#091322', deep: '#060d1b', night: '#101e31', far: '#162d40', city: '#1b3c4c',
  teal: '#275466', line: '#407380', glow: '#7cf4e1', ice: '#cdfcf2',
  blue: '#1689e9', blueDark: '#15499c', blueLight: '#65d4ff', navy: '#112247',
  amber: '#ffc65e', orange: '#f28a47', red: '#fa6170', white: '#e3f5ed',
};

function box(c: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, color: string) {
  if (w <= 0 || h <= 0) return;
  c.fillStyle = color;
  c.fillRect(Math.round(x), Math.round(y), Math.round(w), Math.round(h));
}

function poly(c: CanvasRenderingContext2D, points: number[], color: string) {
  c.fillStyle = color;
  c.beginPath();
  c.moveTo(Math.round(points[0]), Math.round(points[1]));
  for (let n = 2; n < points.length; n += 2) c.lineTo(Math.round(points[n]), Math.round(points[n + 1]));
  c.closePath();
  c.fill();
}

function line(c: CanvasRenderingContext2D, x: number, y: number, x2: number, y2: number, color: string, width = 1) {
  c.strokeStyle = color;
  c.lineWidth = width;
  c.beginPath();
  c.moveTo(Math.round(x) + 0.5, Math.round(y) + 0.5);
  c.lineTo(Math.round(x2) + 0.5, Math.round(y2) + 0.5);
  c.stroke();
}

function text(c: CanvasRenderingContext2D, value: string, x: number, y: number, color = C.white, size = 6, align: CanvasTextAlign = 'left') {
  c.font = `bold ${size}px monospace`;
  c.textAlign = align;
  c.textBaseline = 'top';
  c.fillStyle = color;
  c.fillText(value, Math.round(x), Math.round(y));
}

function random(seed: number) {
  let v = (seed ^ 0x45d9f3b) * 1597334677;
  v = Math.imul(v ^ (v >>> 15), 2246822519);
  return ((v ^ (v >>> 13)) >>> 0) / 4294967295;
}

function disc(c: CanvasRenderingContext2D, x: number, y: number, r: number, color: string) {
  for (let row = -r; row <= r; row += 2) {
    const reach = Math.floor(Math.sqrt(Math.max(0, r * r - row * row)) / 2) * 2;
    box(c, x - reach, y + row, reach * 2, 2, color);
  }
}

function backdrop(c: CanvasRenderingContext2D, state: GameState, now: number) {
  const cx = state.camera.x;
  const cy = state.camera.y;
  box(c, 0, 0, WIDTH, HEIGHT, C.sky);
  box(c, 0, 69, WIDTH, 47, '#0e1d2e');
  box(c, 0, 116, WIDTH, 48, '#142c3c');
  box(c, 0, 164, WIDTH, 52, '#193746');

  for (let i = 0; i < 43; i++) {
    const sx = ((random(i * 13 + 2) * 490 - cx * .035) % 430 + 430) % 430;
    const sy = random(i * 19 + 8) * 105;
    const bright = Math.sin(now * .7 + i * 3) > .7;
    box(c, sx, sy, 1, 1, bright ? '#a7d6d5' : '#45677d');
    if (i % 13 === 0) {
      box(c, sx - 1, sy, 3, 1, '#698e9d');
      box(c, sx, sy - 1, 1, 3, '#698e9d');
    }
  }

  const moonX = 305 - cx * .005;
  disc(c, moonX, 41, 21, '#213e51');
  disc(c, moonX, 41, 18, '#6f9797');
  disc(c, moonX + 5, 37, 17, C.sky);
  box(c, moonX - 15, 39, 3, 4, '#aac5b5');
  box(c, moonX - 10, 51, 5, 2, '#adcabd');

  // Long wisps leave plenty of negative space around the skyline.
  for (let i = 0; i < 7; i++) {
    const xx = ((i * 83 - cx * .06 + now * .15) % 570 + 570) % 570 - 90;
    const yy = 35 + (i % 3) * 24;
    box(c, xx + 13, yy, 51, 2, '#142739');
    box(c, xx, yy + 2, 90, 2, '#162a3a');
    box(c, xx + 36, yy + 4, 63, 1, '#142739');
  }

  for (let layer = 0; layer < 3; layer++) {
    const scroll = cx * [.11, .22, .38][layer];
    const spacing = [22, 34, 49][layer];
    const base = 190 - cy * [.055, .09, .12][layer] + layer * 9;
    const offset = Math.floor(scroll / spacing);
    for (let i = offset - 2; i <= offset + Math.ceil(WIDTH / spacing) + 2; i++) {
      const seed = i * 31 + layer * 997;
      const x = i * spacing - scroll;
      const w = spacing - 3 + Math.floor(random(seed + 3) * 8);
      const h = 29 + Math.floor(random(seed + 5) * (47 + layer * 12));
      const top = base - h;
      const body = ['#142b3d', '#193a4a', '#173c4c'][layer];
      box(c, x, top, w, h + 40, body);
      box(c, x, top, w, 1, ['#1d394a', '#285263', '#2d5968'][layer]);
      box(c, x + w - 3, top + 1, 3, h + 39, '#102c3d');
      const roof = Math.floor(random(seed + 7) * 4);
      if (roof === 0) {
        box(c, x + 6, top - 8, w - 12, 8, body);
        box(c, x + 10, top - 14, 1, 8, C.teal);
        box(c, x + 10, top - 15, 1, 1, Math.sin(now * 2 + i) > 0 ? C.red : '#723d4b');
      } else if (roof === 1) {
        poly(c, [x + 2, top, x + w / 2, top - 10, x + w - 2, top], body);
      }
      for (let col = 4; col < w - 4; col += layer === 0 ? 5 : 7) {
        for (let row = 6; row < h; row += 8) {
          if (random(seed + col * 83 + row) > .52) {
            box(c, x + col, top + row, layer === 0 ? 1 : 2, 3,
              random(seed + row + col) > .83 ? '#97875b' : ['#2d4f5d', '#397180', '#4b7f85'][layer]);
          }
        }
      }
      if (layer === 2 && i % 4 === 0) {
        box(c, x + 4, top + 11, 3, 24, '#347b86');
        box(c, x + 5, top + 13, 1, 19, '#64b5b0');
      }
    }
  }

  // A distant elevated rail, suspension cables, and traffic.
  const railY = 153 - cy * .025;
  const railShift = cx * .28;
  box(c, 0, railY, WIDTH, 3, '#37616c');
  box(c, 0, railY + 3, WIDTH, 4, '#102c3d');
  for (let x = -(railShift % 112); x < WIDTH + 100; x += 112) {
    box(c, x + 30, railY + 5, 7, 70, '#153140');
    line(c, x, railY + 6, x + 32, railY + 28, '#224554', 2);
    line(c, x + 66, railY + 6, x + 34, railY + 28, '#224554', 2);
    box(c, x + 29, railY - 33, 2, 33, '#284c5a');
    line(c, x + 30, railY - 30, x - 15, railY, '#284c5a');
    line(c, x + 30, railY - 30, x + 75, railY, '#284c5a');
  }
  const trainX = ((now * 10 - cx * .28) % 540 + 540) % 540 - 100;
  box(c, trainX, railY - 6, 29, 5, '#426771');
  box(c, trainX + 2, railY - 5, 22, 1, '#94bdad');
  box(c, trainX + 27, railY - 4, 3, 2, C.amber);

  if (cx > 4200 && cx < 7750) industrialBackdrop(c, state, now);
  if (cx > state.level.bossX - 420) arenaBackdrop(c, state, now);
}

function industrialBackdrop(c: CanvasRenderingContext2D, state: GameState, now: number) {
  const scroll = state.camera.x * .65;
  for (let i = Math.floor(scroll / 150) - 1; i < Math.floor(scroll / 150) + 5; i++) {
    const x = i * 150 - scroll;
    const y = 116 - state.camera.y * .12;
    box(c, x, y, 13, 190, '#102733');
    box(c, x + 2, y, 2, 190, '#2d5763');
    box(c, x - 20, y, 109, 7, '#183c49');
    box(c, x - 20, y, 109, 1, '#38606a');
    for (let q = 0; q < 4; q++) {
      line(c, x + q * 22 - 18, y + 1, x + q * 22 - 7, y + 6, '#467078');
      line(c, x + q * 22 - 7, y + 6, x + q * 22 + 4, y + 1, '#467078');
    }
    box(c, x + 75, y + 6, 1, 40, '#496e75');
    poly(c, [x + 71, y + 45, x + 80, y + 45, x + 80, y + 50, x + 77, y + 53, x + 73, y + 50], '#64837c');
    box(c, x + 35, y + 55, 35, 130, '#1e4552');
    box(c, x + 36, y + 54, 33, 2, '#376774');
    box(c, x + 39, y + 59, 3, 120, '#2d5664');
    for (let j = 0; j < 5; j++) box(c, x + 36, y + 69 + j * 23, 34, 2, '#142f3e');
    box(c, x + 48, y + 49, 9, 6, '#243e4c');
    for (let j = 0; j < 3; j++) {
      const rise = ((now * 8 + j * 11 + i * 3) % 32);
      c.globalAlpha = .2 * (1 - rise / 35);
      box(c, x + 46 + Math.sin(now + j) * 2, y + 44 - rise, 12 + rise * .3, 5, '#b4d1c7');
    }
    c.globalAlpha = 1;
  }
}

function arenaBackdrop(c: CanvasRenderingContext2D, state: GameState, now: number) {
  const x = state.level.arena.x + state.level.arena.w / 2 - state.camera.x;
  const y = 230 - state.camera.y;
  box(c, x - 106, y - 29, 212, 142, '#102632');
  box(c, x - 105, y - 29, 210, 3, '#376373');
  box(c, x - 101, y - 23, 202, 2, '#1c3e50');
  for (let i = -3; i <= 3; i++) {
    box(c, x + i * 30 - 10, y - 10, 21, 111, '#173b49');
    box(c, x + i * 30 - 8, y - 8, 2, 107, '#24505b');
    for (let j = 0; j < 8; j++) box(c, x + i * 30 - 6, y + j * 12, 13, 3, '#0d2635');
  }
  box(c, x - 37, y + 6, 74, 34, '#071d2a');
  box(c, x - 35, y + 8, 70, 1, '#4b8790');
  text(c, 'GATE  //  07', x, y + 15, '#75c4bf', 7, 'center');
  text(c, 'RESTRICTED', x, y + 28, '#957653', 5, 'center');
  for (const offset of [-101, 99]) {
    box(c, x + offset, y + 15, 2, 68, '#783e45');
    box(c, x + offset, y + 15, 2, 5, Math.sin(now * 4) > 0 ? C.red : '#a24c4c');
  }
}

function platform(c: CanvasRenderingContext2D, p: Platform, now: number) {
  const { x, y, w, h } = p;
  if (p.kind === 'road') {
    box(c, x, y, w, h, '#0b202c');
    box(c, x, y, w, 2, '#86aaa5');
    box(c, x, y + 2, w, 2, '#345569');
    box(c, x, y + 4, w, Math.min(h - 4, 13), '#243d49');
    box(c, x, y + 16, w, 2, '#4a6470');
    box(c, x, y + 18, w, 3, '#101f2e');
    box(c, x, y + 21, w, 1, '#355667');
    // World-aligned striping and under-deck panels.
    const begin = Math.floor(x / 48) * 48;
    for (let xx = begin; xx < x + w; xx += 48) {
      const left = Math.max(xx + 8, x + 2);
      const right = Math.min(xx + 30, x + w - 2);
      box(c, left, y + 9, right - left, 2, '#bca374');
    }
    for (let xx = x + 3; xx < x + w - 5; xx += 64) {
      const pw = Math.min(58, x + w - xx - 3);
      box(c, xx, y + 25, pw, 26, '#112b38');
      box(c, xx, y + 25, pw, 1, '#284757');
      box(c, xx + 2, y + 28, 1, 18, '#1f3b49');
      box(c, xx + 4, y + 28, 2, 2, '#44616a');
      box(c, xx + pw - 6, y + 46, 2, 2, '#44616a');
      if (pw > 24) {
        poly(c, [xx + 9, y + 48, xx + pw - 10, y + 29, xx + pw - 6, y + 29, xx + 13, y + 48], '#1e3b47');
        box(c, xx + 11, y + 54, pw - 20, Math.max(h - 54, 0), '#0c1d29');
        box(c, xx + 11, y + 54, 3, Math.max(h - 54, 0), '#243d47');
      }
    }
    for (const xx of [x + 1, x + w - 5]) {
      box(c, xx, y + 4, 3, 11, '#708b89');
      box(c, xx, y + 6, 3, 2, '#203749');
      box(c, xx, y + 11, 3, 2, '#203749');
    }
  } else {
    const ledge = p.kind === 'ledge';
    box(c, x, y, w, h, ledge ? '#243d50' : '#1a3341');
    box(c, x, y, w, 2, ledge ? '#81b6ba' : '#769794');
    box(c, x, y + 2, w, 3, '#3b626b');
    box(c, x + 2, y + 5, w - 4, Math.max(h - 7, 1), '#132b3a');
    for (let xx = x + 4; xx < x + w - 5; xx += 16) {
      const ww = Math.min(10, x + w - xx - 2);
      box(c, xx, y + 6, ww, 2, '#a39865');
      if (ww > 6) box(c, xx + 5, y + 6, 3, 2, '#233749');
      if (h > 16) {
        box(c, xx, y + 12, ww, h - 16, '#1d3b47');
        box(c, xx + 1, y + 13, 1, h - 18, '#38535b');
      }
    }
    box(c, x, y + h - 2, w, 2, '#091c2b');
    box(c, x + 1, y + 3, 2, 2, '#afc1af');
    box(c, x + w - 3, y + 3, 2, 2, '#afc1af');
    if (w > 55) {
      box(c, x + w / 2 - 5, y + 10, 10, 3, '#123241');
      box(c, x + w / 2 - 3, y + 10, 5, 1, Math.sin(now * 1.8 + x) > 0 ? '#7fe3c8' : '#3c8d89');
    }
  }
}

function worldDecor(c: CanvasRenderingContext2D, state: GameState) {
  const { camera, level } = state;
  const first = Math.max(0, Math.floor(camera.x / 320) - 1);
  const last = Math.ceil((camera.x + WIDTH) / 320) + 1;
  for (let i = first; i <= last; i++) {
    const x = i * 320 + 176;
    const road = level.platforms.find(p => p.kind === 'road' && p.x <= x && p.x + p.w > x + 36);
    if (!road) continue;
    const y = road.y;
    // Slender roadside light with a broken alternate fixture.
    box(c, x + 4, y - 47, 3, 47, '#102331');
    box(c, x + 5, y - 46, 1, 45, '#41636a');
    box(c, x + 4, y - 48, 24, 3, '#3c5d67');
    box(c, x + 23, y - 46, 9, 2, '#102331');
    box(c, x + 24, y - 45, 7, 1, i % 3 ? '#b5e6cd' : '#6c6c5e');
    if (i % 3) {
      c.globalAlpha = .045;
      poly(c, [x + 24, y - 44, x + 31, y - 44, x + 50, y - 1, x + 4, y - 1], C.glow);
      c.globalAlpha = 1;
    }
    box(c, x + 1, y - 3, 9, 3, '#36515a');
    if (i % 2 === 0) {
      box(c, x + 47, y - 14, 21, 14, '#21394a');
      box(c, x + 49, y - 15, 17, 2, '#426570');
      box(c, x + 50, y - 11, 6, 5, '#0d2533');
      box(c, x + 51, y - 10, 4, 1, '#70bdad');
      for (let j = 0; j < 3; j++) box(c, x + 60, y - 10 + j * 3, 4, 1, '#516773');
    }
    if (i % 4 === 1) {
      box(c, x + 85, y - 69, 3, 69, '#1a3644');
      box(c, x + 122, y - 69, 3, 69, '#1a3644');
      box(c, x + 73, y - 75, 63, 27, '#0a1e2a');
      box(c, x + 74, y - 74, 61, 1, '#588e92');
      box(c, x + 75, y - 73, 59, 1, '#275265');
      text(c, 'ABEL CITY', x + 103, y - 67, '#8ce5d2', 7, 'center');
      text(c, 'HIGHWAY 07  >>', x + 103, y - 57, '#648b93', 5, 'center');
      box(c, x + 74, y - 49, 61, 1, '#305667');
    }
  }
  for (const sign of level.signs) {
    if (sign.x < camera.x - 220 || sign.x > camera.x + WIDTH + 80) continue;
    const width = Math.min(152, Math.max(60, sign.text.length * 3.6 + 14));
    box(c, sign.x - 3, sign.y - 3, width, 14, '#0b2635');
    box(c, sign.x - 3, sign.y - 3, 2, 14, '#5cc9c2');
    box(c, sign.x - 1, sign.y - 3, width - 2, 1, '#31515f');
    text(c, sign.text.toUpperCase(), sign.x + 4, sign.y + 1, '#a6d6cc', 6);
  }
}

function checkpoint(c: CanvasRenderingContext2D, x: number, y: number, active: boolean, now: number) {
  box(c, x - 3, y + 19, 15, 7, '#0b1a2c');
  box(c, x - 2, y + 19, 13, 2, '#617b87');
  box(c, x, y - 12, 9, 31, '#182d47');
  box(c, x + 1, y - 11, 2, 29, '#447a88');
  box(c, x + 3, y - 8, 4, 24, active ? '#367f82' : '#4c495a');
  box(c, x + 4, y - 7, 2, 22, active ? '#82ffdc' : '#a39c82');
  box(c, x - 1, y - 15, 11, 7, '#142b42');
  box(c, x + 1, y - 14, 7, 4, active ? '#83ffdc' : '#e9b766');
  box(c, x + 3, y - 13, 3, 2, '#e5fff2');
  if (active) {
    c.globalAlpha = .1 + Math.sin(now * 3) * .04;
    box(c, x - 5, y - 13, 18, 34, '#62ffdf');
    c.globalAlpha = 1;
    const p = (now * 13) % 30;
    box(c, x - 3, y + 12 - p, 1, 2, C.glow);
    box(c, x + 11, y + 2 - (p + 14) % 30, 1, 2, C.glow);
  }
}

function boot(c: CanvasRenderingContext2D, x: number, y: number, forward: boolean) {
  box(c, x, y, 7, 8, '#10244c');
  box(c, x + 1, y, 5, 5, '#0e66c8');
  box(c, x + 2, y, 3, 3, '#5ac9f6');
  box(c, x + (forward ? 1 : -2), y + 4, 10, 4, '#123879');
  box(c, x + (forward ? 2 : -1), y + 4, 8, 2, '#248de4');
  box(c, x + (forward ? 5 : -1), y + 4, 3, 1, '#88dcff');
  box(c, x + (forward ? 1 : -2), y + 8, 10, 1, '#081d35');
}

function hero(c: CanvasRenderingContext2D, p: Player, elapsed: number, now: number, ghost = false) {
  const run = p.grounded && Math.abs(p.vx) > 25;
  const dash = p.dashTime > 0;
  const airborne = !p.grounded;
  const wall = p.wall !== 0 && p.vy > 0 && !p.grounded;
  const stride = Math.floor(elapsed * 15) % 4;
  const bob = run && stride % 2 ? -1 : p.grounded && !run && Math.sin(now * 2.5) > .85 ? -1 : 0;
  const firing = p.shootCooldown > .055 || p.charge > .12;
  c.save();
  c.translate(Math.round(p.x + p.w / 2), Math.round(p.y + p.h - 29 + bob));
  c.scale(p.facing, 1);

  if (dash) {
    // Low silhouette, trailing rear leg and outstretched cannon.
    box(c, -15, 22, 12, 4, '#113577');
    box(c, -14, 22, 8, 2, C.blueLight);
    boot(c, 3, 20, true);
    box(c, -7, 14, 15, 9, C.navy);
    box(c, -5, 14, 11, 6, '#127edc');
    box(c, -4, 14, 7, 2, C.blueLight);
    c.translate(5, 7);
  } else {
    // A dark rear leg provides separation when both feet are visible.
    const rearX = run ? [-7, -4, -2, -5][stride] : airborne ? -7 : -6;
    const rearY = airborne ? (wall ? 17 : 15) : run ? [20, 19, 18, 20][stride] : 20;
    box(c, rearX + 1, 19, 4, Math.max(rearY - 17, 3), '#3463a6');
    boot(c, rearX, rearY, false);
    box(c, -5, 18, 10, 5, '#0c2b62');
    box(c, -4, 19, 8, 2, '#88c6df');
    const frontX = run ? [3, 1, -5, 0][stride] : airborne ? (wall ? 1 : 2) : 2;
    const frontY = airborne ? (wall ? 19 : 20) : run ? [19, 20, 20, 18][stride] : 20;
    box(c, frontX + 1, 19, 4, 5, '#a1d9e7');
    boot(c, frontX, frontY, true);
    // Back arm and shoulder.
    box(c, -10, 13, 7, 7, '#113979');
    box(c, -9, 13, 5, 3, '#379feb');
    box(c, -9, 19, 4, 5, '#0c56a4');
    box(c, -8, 19, 2, 3, '#78cdf2');
    box(c, -6, 12, 12, 9, '#081e43');
    box(c, -5, 13, 10, 7, '#146bd0');
    box(c, -4, 13, 8, 2, '#57bce9');
    box(c, -1, 16, 5, 4, '#0d4da5');
    box(c, -4, 18, 3, 2, '#8edcef');
  }

  // Helmet: crest, red jewel, armored ear, and visible face.
  box(c, -5, 0, 10, 1, '#0a244a');
  box(c, -7, 1, 13, 2, '#123973');
  box(c, -8, 3, 15, 7, '#092644');
  box(c, -7, 3, 13, 5, '#228ee4');
  box(c, -5, 2, 8, 2, '#60cafd');
  box(c, -5, 4, 3, 3, '#0d6cca');
  box(c, -1, 1, 3, 5, '#cef5ee');
  box(c, 2, 2, 3, 3, '#b83e58');
  box(c, 3, 2, 1, 2, '#ff858b');
  box(c, 1, 6, 6, 6, '#e0b596');
  box(c, 1, 7, 6, 2, '#f8e4bd');
  box(c, 4, 7, 3, 2, '#f1fff1');
  box(c, 5, 7, 1, 2, '#123d5d');
  box(c, 6, 9, 2, 2, '#f1cbaa');
  box(c, 2, 12, 4, 1, '#e8c6a5');
  box(c, -7, 7, 7, 6, '#1069bf');
  box(c, -6, 8, 3, 3, '#77cbed');
  box(c, -5, 9, 2, 2, '#19447f');
  box(c, -2, 8, 2, 5, '#a9e5ef');
  box(c, 0, 12, 2, 2, '#173b72');

  if (firing || dash) {
    box(c, 4, 13, 7, 7, '#153474');
    box(c, 5, 13, 5, 3, '#5bc9f6');
    box(c, 8, 15, 9, 6, '#0c397e');
    box(c, 9, 15, 7, 2, '#299fee');
    box(c, 11, 14, 5, 1, '#77d5fc');
    box(c, 15, 15, 3, 6, '#092749');
    box(c, 16, 16, 1, 3, p.charge > .5 ? '#cdfff0' : '#75cbef');
    if (p.shootCooldown > .13 && !ghost) {
      poly(c, [18, 17, 23, 14, 22, 17, 27, 18, 21, 20, 22, 22, 18, 20], '#fff6be');
    }
  } else if (wall) {
    box(c, 5, 9, 6, 8, '#116cca');
    box(c, 7, 8, 3, 3, '#b4eff4');
    box(c, 8, 5, 4, 4, '#297fd0');
  } else {
    box(c, 5, 13, 7, 6, '#143875');
    box(c, 6, 13, 5, 3, '#5ecbfb');
    box(c, 6, 18, 7, 6, '#0c438e');
    box(c, 7, 18, 5, 3, '#2197e9');
    box(c, 8, 22, 4, 2, '#87d3ec');
  }
  c.restore();
  if (p.charge > .35 && !ghost) {
    const charge = Math.min(p.charge / 1.25, 1);
    const color = p.charge > 1.2 ? '#9dffce' : '#68d8ff';
    for (let i = 0; i < 7; i++) {
      const angle = now * (4 + charge * 3) + i * Math.PI * 2 / 7;
      const x = p.x + p.w / 2 + Math.cos(angle) * (12 + charge * 5);
      const y = p.y + p.h / 2 + Math.sin(angle) * (15 + charge * 4);
      box(c, x, y, 2, 2, color);
      box(c, x - 1, y + 2, 1, 2, '#2d8097');
    }
    if (charge === 1 && Math.floor(now * 12) % 2) {
      box(c, p.x - 3, p.y + 7, 1, 12, color);
      box(c, p.x + p.w + 2, p.y + 9, 1, 9, color);
    }
  }
}

function enemy(c: CanvasRenderingContext2D, e: Enemy, elapsed: number) {
  if (e.hp <= 0) return;
  c.save();
  c.translate(Math.round(e.x + e.w / 2), Math.round(e.y));
  c.scale(e.facing, 1);
  const flash = e.flash > 0;
  const main = flash ? '#ffffff' : e.kind === 'walker' ? '#8d6489' : '#bd864c';
  const lit = flash ? '#ffffff' : e.kind === 'walker' ? '#c699ae' : '#f1ba74';
  if (e.kind === 'walker') {
    const stride = Math.floor(elapsed * 8 + e.id) % 2;
    box(c, -9, 15 + stride, 7, 7 - stride, '#17263e');
    box(c, 3, 15 - stride, 7, 7 + stride, '#17263e');
    box(c, -8, 17 + stride, 5, 3, '#737d8c');
    box(c, 4, 17 - stride, 5, 3, '#9093a0');
    box(c, -11, 20, 9, 2, '#3a4661');
    box(c, 2, 20, 10, 2, '#4d526a');
    box(c, -9, 5, 17, 11, '#22263e');
    box(c, -8, 5, 15, 9, main);
    box(c, -7, 4, 13, 3, lit);
    box(c, -5, 1, 10, 4, '#303044');
    box(c, -4, 0, 8, 2, main);
    box(c, -3, 2, 8, 3, '#101b2c');
    box(c, 2, 2, 4, 2, '#ff785f');
    box(c, 4, 2, 1, 1, '#ffdec0');
    box(c, -5, 9, 5, 4, '#353247');
    box(c, -4, 9, 3, 1, '#eeab97');
    box(c, 7, 7, 6, 7, '#353044');
    box(c, 8, 7, 4, 2, lit);
    box(c, 11, 9, 4, 3, '#1b2a3f');
    box(c, -11, 8, 4, 7, '#57445b');
  } else if (e.kind === 'drone') {
    const flame = Math.floor(elapsed * 25) % 3;
    poly(c, [-8, 13, -4, 13, -5, 18 + flame, -7, 16], '#f39468');
    poly(c, [4, 13, 8, 13, 7, 18 + (2 - flame), 5, 16], '#f39468');
    box(c, -11, 6, 22, 8, '#15253b');
    box(c, -10, 5, 20, 6, main);
    box(c, -7, 2, 14, 6, main);
    box(c, -6, 2, 12, 2, lit);
    box(c, -12, 7, 4, 4, '#537184');
    box(c, 8, 7, 4, 4, '#779196');
    box(c, -6, 8, 13, 4, '#183348');
    box(c, -3, 8, 8, 3, '#ef6e72');
    box(c, 1, 8, 3, 2, '#ffe3b6');
    box(c, -4, 13, 8, 3, '#344655');
    box(c, -1, 0, 2, 3, '#627c84');
  } else {
    box(c, -11, 20, 22, 5, '#162536');
    box(c, -10, 20, 20, 2, '#546971');
    box(c, -7, 9, 14, 12, '#224e62');
    box(c, -6, 10, 12, 3, '#6b9698');
    box(c, -4, 14, 8, 5, '#142f43');
    box(c, -3, 15, 6, 1, '#559499');
    box(c, -8, 4, 15, 8, main);
    box(c, -6, 2, 11, 4, lit);
    box(c, -5, 5, 8, 4, '#273342');
    box(c, 0, 6, 4, 2, '#ff8164');
    box(c, 6, 5, 10, 5, '#20374a');
    box(c, 7, 5, 8, 1, '#6b8e97');
    box(c, 14, 4, 3, 7, '#344b5f');
    box(c, 16, 6, 1, 3, '#ffcf83');
  }
  c.restore();
}

function boss(c: CanvasRenderingContext2D, b: Boss, now: number) {
  if (b.phase === 'defeated' || b.hp <= 0) return;
  c.save();
  c.translate(Math.round(b.x + b.w / 2), Math.round(b.y));
  c.scale(b.facing, 1);
  // Fit the 60-row design to the collider, keeping every resulting edge on a pixel.
  const bossBox = (ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, color: string) => {
    const top = Math.round(y * b.h / 60);
    box(ctx, x, top, w, Math.max(1, Math.round((y + h) * b.h / 60) - top), color);
  };
  const bossPoly = (ctx: CanvasRenderingContext2D, points: number[], color: string) => {
    poly(ctx, points.map((value, index) => index % 2 ? Math.round(value * b.h / 60) : value), color);
  };
  const orange = b.flash > 0 ? '#fff3d5' : '#c87842';
  const highlight = b.flash > 0 ? '#ffffff' : '#ffbf73';
  const shade = '#784852';
  const pulse = Math.sin(now * (b.hp < b.maxHp / 2 ? 17 : 8)) > 0;
  const warning = b.phase.startsWith('windup');
  const stride = b.phase === 'dash' ? Math.floor(now * 20) % 2 * 3 : 0;
  bossBox(c, -22, 43 - stride, 16, 16, '#111f35');
  bossBox(c, 6, 43 + stride, 16, 15 - stride, '#111f35');
  bossBox(c, -21, 44 - stride, 14, 11, shade);
  bossBox(c, 7, 44 + stride, 14, 10 - stride, orange);
  bossBox(c, -20, 44 - stride, 11, 3, orange);
  bossBox(c, 8, 44 + stride, 11, 3, highlight);
  bossBox(c, -25, 54, 20, 6, '#334456');
  bossBox(c, 6, 54, 22, 6, '#3e4d5c');
  bossBox(c, -24, 54, 17, 2, '#73817e');
  bossBox(c, 7, 54, 19, 2, '#939e93');
  bossBox(c, -14, 35, 28, 13, '#182c43');
  bossBox(c, -11, 37, 23, 5, '#798987');
  bossBox(c, -8, 39, 5, 2, '#263b4c');
  bossBox(c, 2, 39, 7, 2, '#263b4c');
  bossPoly(c, [-19, 14, 16, 14, 21, 30, 12, 39, -14, 38, -22, 27], '#101c30');
  bossPoly(c, [-17, 15, 15, 15, 18, 29, 10, 36, -12, 35, -19, 26], orange);
  bossBox(c, -15, 15, 28, 3, highlight);
  bossBox(c, -12, 22, 23, 11, '#2c3648');
  bossBox(c, -9, 24, 18, 6, '#10263d');
  bossBox(c, -6, 25, 12, 4, warning && pulse ? '#ffffff' : '#74e5d0');
  bossBox(c, -3, 25, 6, 2, '#d5ffee');
  bossBox(c, -12, 32, 22, 2, shade);
  bossBox(c, -27, 15, 15, 17, '#17273d');
  bossBox(c, -26, 15, 13, 11, orange);
  bossBox(c, -24, 14, 9, 3, highlight);
  bossBox(c, -25, 29, 12, 14, '#324659');
  bossBox(c, -25, 30, 3, 10, '#839797');
  bossBox(c, -27, 39, 14, 7, '#182c43');
  bossBox(c, 13, 14, 17, 16, '#15243b');
  bossBox(c, 14, 15, 15, 10, orange);
  bossBox(c, 16, 14, 11, 3, highlight);
  bossBox(c, 17, 25, 12, 7, '#566b75');
  bossBox(c, 17, 28, 19, 13, '#192c41');
  bossBox(c, 18, 29, 15, 10, '#486073');
  bossBox(c, 19, 29, 13, 2, '#819d9e');
  bossBox(c, 22, 34, 10, 2, '#11283c');
  bossBox(c, 31, 27, 7, 16, '#233d53');
  bossBox(c, 35, 30, 3, 10, warning && pulse ? C.amber : '#85c8c4');
  // Heavy helmet, swept horns, and a narrow, hot visor.
  bossBox(c, -11, 1, 21, 15, '#101e32');
  bossBox(c, -9, 2, 17, 11, orange);
  bossBox(c, -7, 1, 13, 3, highlight);
  bossBox(c, -4, 0, 4, 5, '#789693');
  bossPoly(c, [-10, 4, -17, -2, -14, 8, -9, 10], '#839593');
  bossPoly(c, [8, 4, 15, -2, 12, 8, 7, 10], '#b7bcb0');
  bossBox(c, -8, 7, 16, 5, '#15233a');
  bossBox(c, -5, 8, 13, 2, b.hp < b.maxHp / 2 ? '#ff6870' : '#ffc05d');
  bossBox(c, 2, 8, 5, 1, '#fff3c0');
  bossBox(c, -5, 12, 10, 5, '#4f6573');
  for (let i = 0; i < 3; i++) bossBox(c, -3 + i * 3, 13, 1, 3, '#15293d');
  if (warning) {
    const flick = Math.floor(now * 12) % 2;
    text(c, '!', 0, -16 - flick, C.amber, 12, 'center');
    bossBox(c, -15, -9, 5, 1, C.orange);
    bossBox(c, 11, -9, 5, 1, C.orange);
  }
  c.restore();
}

function hud(c: CanvasRenderingContext2D, state: GameState, now: number) {
  if (state.mode === 'title') return;
  const p = state.player;
  // The classic segmented life column, set in an original metal housing.
  box(c, 8, 10, 17, 9, '#071526');
  text(c, 'X', 16, 9, '#80e7ee', 11, 'center');
  box(c, 9, 22, 15, 94, '#07121e');
  box(c, 9, 22, 2, 94, '#496b76');
  box(c, 22, 22, 2, 94, '#223f53');
  box(c, 10, 22, 13, 2, '#95b8ae');
  box(c, 10, 114, 13, 2, '#5b8087');
  const segments = 22;
  for (let i = 0; i < segments; i++) {
    const y = 110 - i * 4;
    const full = i < Math.ceil(p.hp / p.maxHp * segments);
    box(c, 12, y, 9, 3, full ? (p.hp <= p.maxHp * .25 ? '#f59066' : '#e6d781') : '#1b3442');
    if (full) box(c, 12, y, 9, 1, p.hp <= p.maxHp * .25 ? '#ffc691' : '#fff0b1');
  }
  if (p.charge > .15) {
    box(c, 11, 120, 12, 3, '#233e4c');
    box(c, 11, 120, 12 * Math.min(p.charge / 1.25, 1), 3, p.charge > 1.2 ? '#8affc6' : '#63cfff');
    if (p.charge > 1.2 && Math.floor(now * 8) % 2) box(c, 10, 119, 14, 1, '#defbe9');
  }
  const seconds = Math.floor(state.elapsed);
  const clock = `${Math.floor(seconds / 60).toString().padStart(2, '0')}:${(seconds % 60).toString().padStart(2, '0')}`;
  box(c, WIDTH - 102, 9, 93, 24, '#081727');
  box(c, WIDTH - 102, 9, 2, 24, '#376d7b');
  text(c, 'MISSION', WIDTH - 95, 13, '#638d9b', 5);
  text(c, clock, WIDTH - 15, 12, '#d2e9d8', 8, 'right');
  const progress = Math.min(1, Math.max(0, (p.x - 64) / (state.level.width - 250)));
  box(c, WIDTH - 95, 26, 79, 2, '#1d3949');
  box(c, WIDTH - 95, 26, 79 * progress, 2, '#5dbeb9');
  const sector = p.x < 4750 ? '01 / RUINED HIGHWAY' : p.x < 7400 ? '02 / INDUSTRIAL SPINE' : '03 / COLOSSUS GATE';
  text(c, sector, WIDTH - 11, 38, '#769ba7', 5, 'right');

  if (state.boss.active && state.boss.hp > 0) {
    const b = state.boss;
    box(c, 94, 192, 199, 16, '#081425');
    text(c, 'IRON COLOSSUS', 99, 195, '#efc4a1', 5);
    box(c, 158, 196, 129, 6, '#48313d');
    for (let i = 0; i < 32; i++) {
      if (i < Math.ceil(b.hp / b.maxHp * 32)) {
        box(c, 159 + i * 4, 197, 3, 4, b.hp < b.maxHp / 2 ? '#ec7770' : '#f1b75c');
        box(c, 159 + i * 4, 197, 3, 1, '#ffe1ab');
      }
    }
    if (b.phase.startsWith('windup')) {
      const cue = b.phase === 'windup-dash' ? 'DASH INCOMING — JUMP' : b.phase === 'windup-shot' ? 'CANNON CHARGING' : 'GROUND SLAM — KEEP MOVING';
      text(c, cue, WIDTH / 2, 179, Math.floor(now * 5) % 2 ? '#ffe0a7' : '#d59871', 6, 'center');
    }
  }
  if (state.notice && state.noticeTime > 0 && state.mode === 'playing') {
    c.globalAlpha = Math.min(1, state.noticeTime * 2);
    const width = Math.min(260, state.notice.length * 4.3 + 24);
    box(c, (WIDTH - width) / 2, 48, width, 18, '#0b2433');
    box(c, (WIDTH - width) / 2, 48, 2, 18, C.glow);
    text(c, state.notice.toUpperCase(), WIDTH / 2, 54, '#a7e9d5', 6, 'center');
    c.globalAlpha = 1;
  }
}

export function renderGame(c: CanvasRenderingContext2D, state: GameState, now: number): void {
  c.save();
  c.imageSmoothingEnabled = false;
  c.setTransform(1, 0, 0, 1, 0, 0);
  c.globalAlpha = 1;
  backdrop(c, state, now);
  c.save();
  const shake = state.camera.shake;
  const sx = shake > 0 ? Math.sin(now * 173) * Math.min(shake * 14, 3) : 0;
  const sy = shake > 0 ? Math.cos(now * 137) * Math.min(shake * 12, 2) : 0;
  c.translate(-Math.round(state.camera.x - sx), -Math.round(state.camera.y - sy));
  const visible = (x: number, w: number) => x + w > state.camera.x - 50 && x < state.camera.x + WIDTH + 50;

  worldDecor(c, state);
  for (const p of state.level.platforms) if (visible(p.x, p.w)) platform(c, p, now);
  for (const h of state.level.hazards) {
    if (!visible(h.x, h.w)) continue;
    box(c, h.x, h.y + h.h - 4, h.w, 4, '#52314a');
    for (let x = h.x; x < h.x + h.w; x += 7) {
      poly(c, [x, h.y + h.h - 3, x + 3, h.y, x + 7, h.y + h.h - 3], '#81999e');
      line(c, x + 3, h.y + 2, x + 4, h.y + h.h - 4, '#dde2c7');
    }
  }
  state.level.checkpoints.forEach((point, index) => {
    if (index > 0 && visible(point.x, 20)) checkpoint(c, point.x, point.y, index <= state.checkpoint, now);
  });
  for (const pickup of state.pickups) {
    if (!visible(pickup.x, pickup.w)) continue;
    const bob = Math.sin(now * 4 + pickup.x) * 2;
    const x = pickup.x + pickup.w / 2;
    const y = pickup.y + bob;
    box(c, x - 6, y, 12, 11, '#133844');
    box(c, x - 5, y, 10, 1, '#a7f4cf');
    box(c, x - 5, y + 1, 10, 9, '#39a88d');
    box(c, x - 2, y + 2, 4, 7, '#d2ffe1');
    box(c, x - 4, y + 4, 8, 3, '#d2ffe1');
    box(c, x - 4, y + 11, 8, 1, '#0b222f');
  }

  if (state.boss.active && state.boss.phase === 'windup-slam') {
    const floor = state.level.arena.y + state.level.arena.h;
    const targetCenter = state.boss.targetX + state.boss.w / 2;
    c.globalAlpha = .3 + Math.sin(now * 20) * .15;
    box(c, targetCenter - 28, floor - 2, 56, 2, C.red);
    for (let i = 0; i < 5; i++) box(c, targetCenter - 24 + i * 10, floor - 5, 5, 2, C.orange);
    c.globalAlpha = 1;
  }
  for (const e of state.enemies) if (visible(e.x, e.w)) enemy(c, e, state.elapsed);
  if (visible(state.boss.x, state.boss.w)) boss(c, state.boss, now);

  const p = state.player;
  if (p.dashTime > 0) {
    for (let i = 3; i >= 1; i--) {
      c.globalAlpha = .2 * (1 - i * .15);
      hero(c, { ...p, x: p.x - p.facing * i * 10 }, state.elapsed, now, true);
    }
    c.globalAlpha = 1;
    for (let i = 0; i < 3; i++) box(c, p.x - p.facing * (14 + i * 7), p.y + 14 + i * 4, 7 + i * 3, 1, '#81ddf4');
  }
  if (!(p.invulnerable > 0 && Math.floor(now * 18) % 3 === 0)) {
    hero(c, state.mode === 'title' ? { ...p, grounded: true } : p, state.elapsed, now);
  }

  for (const shot of state.shots) {
    if (!visible(shot.x, shot.w)) continue;
    const x = shot.x, y = shot.y, w = Math.max(shot.w, 5), h = Math.max(shot.h, 3);
    if (shot.owner === 'player' && shot.charged > 0) {
      const full = shot.charged >= 2;
      const outer = full ? '#3abcad' : '#298ad4';
      const mid = full ? '#95f5c5' : '#7bcffc';
      const trailing = shot.vx > 0 ? x - 11 : x + w;
      box(c, trailing, y + h / 2 - 2, 11, 4, outer);
      poly(c, [x - 2, y + h / 2, x + 3, y - 2, x + w - 3, y - 2, x + w + 3, y + h / 2, x + w - 3, y + h + 2, x + 3, y + h + 2], outer);
      box(c, x, y + 1, w, h - 2, mid);
      box(c, x + 2, y + h / 2 - 2, w - 2, 4, '#f4ffdb');
      box(c, x + 4, y - 2, Math.max(w - 8, 3), 1, '#d4ffdf');
    } else {
      const friendly = shot.owner === 'player';
      box(c, x - 1, y + 1, w + 2, Math.max(1, h - 2), friendly ? '#db8c46' : '#a64761');
      box(c, x + 1, y, Math.max(2, w - 2), h, friendly ? '#ffe1a0' : '#ff8c7a');
      box(c, x + 1, y + 1, Math.max(2, w - 3), Math.max(1, h - 2), '#fff5d2');
    }
  }
  for (const part of state.particles) {
    c.globalAlpha = Math.min(1, part.life / Math.max(part.maxLife, .001) * 1.7);
    box(c, part.x, part.y, part.size, part.size, part.color);
    if (part.size >= 4) box(c, part.x + 1, part.y + 1, part.size - 2, part.size - 2, '#fff4c2');
  }
  c.globalAlpha = 1;
  c.restore();
  hud(c, state, now);

  // Subtle CRT scan lines are part of the artwork, never a readability filter.
  c.globalAlpha = .055;
  for (let y = 1; y < HEIGHT; y += 2) box(c, 0, y, WIDTH, 1, '#00040c');
  c.restore();
}
