/**
 * Software raycast renderer (column DDA) with:
 *  - textured walls, one gpt-image-2 panel per block, per-block brightness jitter
 *  - sliding doors rendered at the cell midline (Wolf3D style)
 *  - textured floor & ceiling casting
 *  - exponential distance fog to near-black, N/S vs E/W shading
 *  - per-cell light zones (flicker / hell-pulse / dim) + muzzle light boost
 *  - z-buffered billboard sprites (enemies, pickups, puffs, projectiles)
 *  - gun viewmodel with sway/kick, muzzle flash overlay, damage vignette, grain
 */
import type { Assets, Tex } from './assets';
import type { Game } from './game';
import { CELL } from './level';

export const VIEW_W = 240;
export const VIEW_H = 320;
const FOV_PLANE = 0.66; // ~67° horizontal

interface SpriteDraw {
  tex: Tex;
  x: number;
  y: number;
  worldH: number;
  worldW: number;
  /** bottom anchor in world units above floor */
  lift: number;
  dist: number;
  flash: number;
  fade: number; // 1 normal, <1 darken+ghost (corpses, puffs)
  squash: number; // vertical scale (death squash)
}

export class Renderer {
  private buf: ImageData;
  private u32: Uint32Array;
  private inner: HTMLCanvasElement;
  private ictx: CanvasRenderingContext2D;
  private octx: CanvasRenderingContext2D;
  private zbuf = new Float32Array(VIEW_W);
  private zoneLight: Float32Array;
  private grain: HTMLCanvasElement;

  constructor(private out: HTMLCanvasElement, private assets: Assets, private game: Game) {
    this.inner = document.createElement('canvas');
    this.inner.width = VIEW_W;
    this.inner.height = VIEW_H;
    this.ictx = this.inner.getContext('2d')!;
    this.octx = out.getContext('2d')!;
    this.octx.imageSmoothingEnabled = false;
    this.buf = this.ictx.createImageData(VIEW_W, VIEW_H);
    this.u32 = new Uint32Array(this.buf.data.buffer);
    this.zoneLight = new Float32Array(game.level.w * game.level.h);
    this.grain = document.createElement('canvas');
    this.grain.width = 128;
    this.grain.height = 128;
    const gctx = this.grain.getContext('2d')!;
    const gd = gctx.createImageData(128, 128);
    for (let i = 0; i < gd.data.length; i += 4) {
      const v = (Math.random() * 255) | 0;
      gd.data[i] = v; gd.data[i + 1] = v; gd.data[i + 2] = v;
      gd.data[i + 3] = 22;
    }
    gctx.putImageData(gd, 0, 0);
  }

  private fog(dist: number): number {
    const f = 1.22 * Math.exp(-dist * 0.135);
    return f > 1.08 ? 1.08 : f;
  }

  private updateZoneLight(t: number): void {
    const g = this.game;
    const l = g.level;
    this.zoneLight.fill(1);
    for (const z of g.level.zones) {
      let v = 1;
      if (z.mode === 'pulse') {
        v = 0.62 + 0.38 * (0.5 + 0.5 * Math.sin(t * 1.7 + z.seed));
      } else if (z.mode === 'flicker') {
        // harsh broken-ballast flicker
        const n = Math.sin(t * 31 + z.seed) * Math.sin(t * 7.3 + z.seed * 2) + Math.sin(t * 53);
        v = n > 0.4 ? 1.05 : 0.38;
      } else {
        v = 0.55 + 0.1 * Math.sin(t * 2.2 + z.seed);
      }
      for (let y = z.y0; y <= z.y1; y++) {
        for (let x = z.x0; x <= z.x1; x++) {
          this.zoneLight[y * l.w + x] = v;
        }
      }
    }
  }

  render(t: number): void {
    const g = this.game;
    const l = g.level;
    this.updateZoneLight(t);
    const boost = 1 + g.muzzle * 0.85;
    const dirX = Math.cos(g.angle), dirY = Math.sin(g.angle);
    const planeX = -dirY * FOV_PLANE, planeY = dirX * FOV_PLANE;
    const posX = g.px, posY = g.py;
    const u32 = this.u32;
    const halfH = VIEW_H / 2;
    // head bob: subtle horizon shift
    const bobY = Math.sin(g.walkCycle) * 2.2 * g.moving;
    const horizon = (halfH + bobY) | 0;

    // ---------- floor & ceiling casting ----------
    const floorTex = this.assets.floor, ceilTex = this.assets.ceil;
    const fpx = floorTex.px, cpx = ceilTex.px;
    const ftw = floorTex.w, cth = ceilTex.w;
    const rayX0 = dirX - planeX, rayY0 = dirY - planeY;
    const rayX1 = dirX + planeX, rayY1 = dirY + planeY;
    const zl = this.zoneLight;
    for (let y = 0; y < VIEW_H; y++) {
      const isFloor = y > horizon;
      const p = isFloor ? y - horizon : horizon - y;
      if (p <= 0) {
        // exact horizon: near-black
        const rowOff = y * VIEW_W;
        for (let x = 0; x < VIEW_W; x++) u32[rowOff + x] = 0xff050403;
        continue;
      }
      const rowDist = halfH / p;
      const shadeBase = this.fog(rowDist) * boost * (isFloor ? 1 : 0.62);
      if (shadeBase < 0.012) {
        const rowOff = y * VIEW_W;
        const c = isFloor ? 0xff060504 : 0xff040303;
        for (let x = 0; x < VIEW_W; x++) u32[rowOff + x] = c;
        continue;
      }
      const stepX = (rowDist * (rayX1 - rayX0)) / VIEW_W;
      const stepY = (rowDist * (rayY1 - rayY0)) / VIEW_W;
      let wx = posX + rowDist * rayX0;
      let wy = posY + rowDist * rayY0;
      const rowOff = y * VIEW_W;
      const tex = isFloor ? fpx : cpx;
      const tw = isFloor ? ftw : cth;
      for (let x = 0; x < VIEW_W; x++) {
        const cellX = wx | 0, cellY = wy | 0;
        let s = shadeBase;
        if (cellX >= 0 && cellY >= 0 && cellX < l.w && cellY < l.h) {
          s *= zl[cellY * l.w + cellX]!;
        }
        const tx = ((wx - cellX) * tw) | 0;
        const ty = ((wy - cellY) * tw) | 0;
        const px = tex[ty * tw + tx]!;
        let r = (px & 0xff) * s, gg = ((px >>> 8) & 0xff) * s, b = ((px >>> 16) & 0xff) * s;
        if (r > 255) r = 255; if (gg > 255) gg = 255; if (b > 255) b = 255;
        u32[rowOff + x] = 0xff000000 | (b << 16) | (gg << 8) | r;
        wx += stepX;
        wy += stepY;
      }
    }

    // ---------- walls ----------
    const grid = l.grid, lw = l.w, lh = l.h;
    for (let x = 0; x < VIEW_W; x++) {
      const camX = (2 * x) / VIEW_W - 1;
      const rdx = dirX + planeX * camX;
      const rdy = dirY + planeY * camX;
      let mapX = posX | 0, mapY = posY | 0;
      const deltaX = Math.abs(1 / (rdx || 1e-9));
      const deltaY = Math.abs(1 / (rdy || 1e-9));
      const stepGX = rdx < 0 ? -1 : 1;
      const stepGY = rdy < 0 ? -1 : 1;
      let sideX = rdx < 0 ? (posX - mapX) * deltaX : (mapX + 1 - posX) * deltaX;
      let sideY = rdy < 0 ? (posY - mapY) * deltaY : (mapY + 1 - posY) * deltaY;
      let side = 0;
      let perpDist = 32;
      let tex: Tex | null = null;
      let wallX = 0;
      let cellIdx = 0;

      for (let i = 0; i < 64; i++) {
        if (sideX < sideY) {
          sideX += deltaX; mapX += stepGX; side = 0;
        } else {
          sideY += deltaY; mapY += stepGY; side = 1;
        }
        if (mapX < 0 || mapY < 0 || mapX >= lw || mapY >= lh) {
          perpDist = 32;
          break;
        }
        cellIdx = mapY * lw + mapX;
        const c = grid[cellIdx]!;
        if (c === CELL.FLOOR) continue;
        if (c === CELL.DOOR || c === CELL.LOCKED) {
          const d = g.doorAt(mapX, mapY);
          const open = d ? d.open : 0;
          // door plane sits at the middle of the cell
          const dMid = side === 0 ? sideX - deltaX * 0.5 : sideY - deltaY * 0.5;
          const exitDist = side === 0 ? sideY : sideX;
          if (dMid < exitDist) {
            const hitCoord = side === 0 ? posY + dMid * rdy : posX + dMid * rdx;
            let u = hitCoord - Math.floor(hitCoord);
            u -= open; // door slides into the jamb
            if (u >= 0) {
              perpDist = dMid;
              tex = this.assets.door;
              wallX = u;
              break;
            }
          }
          continue; // ray passes through the opening (or misses midplane)
        }
        // solid wall
        perpDist = side === 0 ? sideX - deltaX : sideY - deltaY;
        tex = this.assets.walls[c] ?? this.assets.walls[1]!;
        const hitCoord = side === 0 ? posY + perpDist * rdy : posX + perpDist * rdx;
        wallX = hitCoord - Math.floor(hitCoord);
        if ((side === 0 && rdx > 0) || (side === 1 && rdy < 0)) wallX = 1 - wallX;
        break;
      }

      this.zbuf[x] = perpDist;
      if (!tex) continue;
      const lineH = (VIEW_H / perpDist) | 0;
      let y0 = horizon - (lineH >> 1);
      let y1 = horizon + (lineH >> 1);
      const texX = Math.min(tex.w - 1, (wallX * tex.w) | 0);
      let shade = this.fog(perpDist) * boost * (l.jitter[cellIdx] ?? 1) * (zl[cellIdx] ?? 1);
      if (side === 1) shade *= 0.74;
      if (shade > 2) shade = 2;
      const texStep = tex.h / lineH;
      let texPos = y0 < 0 ? -y0 * texStep : 0;
      if (y0 < 0) y0 = 0;
      if (y1 > VIEW_H) y1 = VIEW_H;
      const tpx = tex.px;
      const tw = tex.w;
      for (let y = y0; y < y1; y++) {
        const ty = texPos | 0;
        texPos += texStep;
        const px = tpx[(ty >= tex.h ? tex.h - 1 : ty) * tw + texX]!;
        let r = (px & 0xff) * shade, gg = ((px >>> 8) & 0xff) * shade, b = ((px >>> 16) & 0xff) * shade;
        if (r > 255) r = 255; if (gg > 255) gg = 255; if (b > 255) b = 255;
        u32[y * VIEW_W + x] = 0xff000000 | (b << 16) | (gg << 8) | r;
      }
    }

    // ---------- sprites ----------
    const sprites: SpriteDraw[] = [];
    const a = this.assets;
    for (const e of this.game.enemies) {
      const dx = e.x - posX, dy = e.y - posY;
      const dist = Math.hypot(dx, dy);
      if (dist > 22) continue;
      const tex = e.kind === 'husk' ? a.husk : e.kind === 'stalker' ? a.stalker : a.brute;
      if (e.state === 'dead') {
        const k = Math.min(1, e.deathT / 0.45);
        sprites.push({
          tex, x: e.x, y: e.y, worldH: e.worldH, worldW: e.worldH,
          lift: 0, dist, flash: 0, fade: 0.55 - k * 0.25, squash: 1 - k * 0.78,
        });
      } else {
        const bob = Math.sin(t * 3.1 + e.bobSeed) * 0.015 +
          (e.state === 'chase' ? Math.abs(Math.sin(t * 6 + e.bobSeed)) * 0.03 : 0);
        sprites.push({
          tex, x: e.x, y: e.y, worldH: e.worldH, worldW: e.worldH,
          lift: bob, dist, flash: e.flash, fade: 1, squash: 1,
        });
      }
    }
    for (const p of this.game.pickups) {
      if (p.taken) continue;
      const dist = Math.hypot(p.x - posX, p.y - posY);
      if (dist > 20) continue;
      const tex = p.kind === 'medkit' ? a.medkit : p.kind === 'ammo' ? a.ammo : a.keycard;
      const hover = p.kind === 'keycard' ? 0.18 + Math.sin(t * 2.5) * 0.05 : 0.02;
      const size = p.kind === 'keycard' ? 0.26 : 0.34;
      sprites.push({
        tex, x: p.x, y: p.y, worldH: size, worldW: size,
        lift: hover, dist, flash: p.kind === 'keycard' ? 0.12 + 0.1 * Math.sin(t * 4) : 0,
        fade: 1, squash: 1,
      });
    }
    {
      const dist = Math.hypot(l.exitX - posX, l.exitY - posY);
      if (dist < 22) {
        sprites.push({
          tex: a.exit, x: l.exitX, y: l.exitY, worldH: 0.85, worldW: 0.85,
          lift: 0.05 + Math.sin(t * 2) * 0.03, dist, flash: 0, fade: 1, squash: 1,
        });
      }
    }
    for (const pr of this.game.projectiles) {
      const dist = Math.hypot(pr.x - posX, pr.y - posY);
      sprites.push({
        tex: a.projectile, x: pr.x, y: pr.y, worldH: 0.22, worldW: 0.22,
        lift: 0.35, dist, flash: 0, fade: 1, squash: 1,
      });
    }
    for (const pf of this.game.puffs) {
      const dist = Math.hypot(pf.x - posX, pf.y - posY);
      const k = pf.t / pf.max;
      const size = (pf.kind === 'blood' ? 0.12 : 0.08) + k * 0.13;
      sprites.push({
        tex: pf.kind === 'blood' ? a.puffBlood : a.puffSpark,
        x: pf.x, y: pf.y, worldH: size, worldW: size,
        lift: pf.z, dist, flash: 0, fade: (1 - k) * 0.8, squash: 1,
      });
    }
    sprites.sort((s1, s2) => s2.dist - s1.dist);

    const invDet = 1 / (planeX * dirY - dirX * planeY);
    for (const s of sprites) {
      const relX = s.x - posX, relY = s.y - posY;
      const trX = invDet * (dirY * relX - dirX * relY);
      const trY = invDet * (-planeY * relX + planeX * relY);
      if (trY <= 0.05) continue;
      const screenX = (VIEW_W / 2) * (1 + trX / trY);
      const scale = VIEW_H / trY;
      const hPix = s.worldH * scale * s.squash;
      const wPix = s.worldW * scale;
      // floor line at this depth, lifted by anchor
      const floorY = horizon + (halfH / trY);
      const bottom = floorY - s.lift * scale;
      const top = bottom - hPix;
      let x0 = Math.ceil(screenX - wPix / 2);
      let x1 = Math.floor(screenX + wPix / 2);
      const fullW = wPix;
      const srcX0 = screenX - wPix / 2;
      if (x0 < 0) x0 = 0;
      if (x1 >= VIEW_W) x1 = VIEW_W - 1;
      let y0 = Math.ceil(top);
      let y1 = Math.floor(bottom);
      if (y0 < 0) y0 = 0;
      if (y1 >= VIEW_H) y1 = VIEW_H - 1;
      if (x0 > x1 || y0 > y1) continue;
      const cellX = s.x | 0, cellY = s.y | 0;
      let shade = this.fog(trY) * boost;
      if (cellX >= 0 && cellY >= 0 && cellX < lw && cellY < lh) {
        shade *= zl[cellY * lw + cellX]!;
      }
      shade *= s.fade;
      if (shade > 1.6) shade = 1.6;
      const fl = s.flash;
      const tpx = s.tex.px, tw = s.tex.w, th = s.tex.h;
      const alphaScale = s.fade < 0.45 ? s.fade / 0.45 : 1; // ghost very faded puffs
      for (let x = x0; x <= x1; x++) {
        if (this.zbuf[x]! <= trY) continue;
        const tx = (((x - srcX0) / fullW) * tw) | 0;
        if (tx < 0 || tx >= tw) continue;
        for (let y = y0; y <= y1; y++) {
          const ty = (((y - top) / (bottom - top)) * th) | 0;
          if (ty < 0 || ty >= th) continue;
          const px = tpx[ty * tw + tx]!;
          const pa = px >>> 24;
          if (pa < 60) continue;
          if (alphaScale < 1 && pa * alphaScale < 50) continue;
          let r = (px & 0xff) * shade, gg = ((px >>> 8) & 0xff) * shade, b = ((px >>> 16) & 0xff) * shade;
          if (fl > 0) {
            r += (255 - r) * fl; gg += (255 - gg) * fl; b += (255 - b) * fl;
          }
          if (r > 255) r = 255; if (gg > 255) gg = 255; if (b > 255) b = 255;
          u32[y * VIEW_W + x] = 0xff000000 | (b << 16) | (gg << 8) | r;
        }
      }
    }

    // blit pixels, then canvas-level overlays
    this.ictx.putImageData(this.buf, 0, 0);
    this.drawOverlays(t);

    // upscale to output canvas (with screen shake)
    const o = this.octx;
    o.imageSmoothingEnabled = false;
    const shake = this.game.shake;
    const sx = shake > 0 ? (Math.random() - 0.5) * shake * 9 : 0;
    const sy = shake > 0 ? (Math.random() - 0.5) * shake * 9 : 0;
    o.fillStyle = '#000';
    o.fillRect(0, 0, this.out.width, this.out.height);
    o.drawImage(this.inner, sx, sy, this.out.width, this.out.height);
  }

  private drawOverlays(t: number): void {
    const ctx = this.ictx;
    const g = this.game;

    // --- gun viewmodel ---
    const gun = this.assets.gun;
    if (gun && (g.phase === 'playing' || g.phase === 'won')) {
      const sway = g.moving;
      const gx = VIEW_W / 2 + Math.sin(g.walkCycle * 0.5) * 9 * sway;
      const kick = g.gunKick;
      const gy = VIEW_H + Math.abs(Math.cos(g.walkCycle * 0.5)) * 7 * sway + 10 + kick * 16;
      const size = 205;
      ctx.save();
      ctx.translate(gx, gy);
      ctx.rotate(kick * 0.06 + Math.sin(g.walkCycle * 0.5) * 0.01 * sway);
      // darken gun with ambient: simple alpha black overlay after draw
      ctx.drawImage(gun, -size / 2, -size, size, size);
      ctx.restore();

      // muzzle flash overlay + glow, anchored to the barrel tip
      if (g.muzzle > 0.05) {
        const mx = gx + 2;
        const my = gy - size * 0.5;
        const rad = 20 + g.muzzle * 22;
        const grd = ctx.createRadialGradient(mx, my, 2, mx, my, rad);
        grd.addColorStop(0, `rgba(255,245,200,${0.95 * g.muzzle})`);
        grd.addColorStop(0.35, `rgba(255,180,70,${0.7 * g.muzzle})`);
        grd.addColorStop(1, 'rgba(255,120,20,0)');
        ctx.fillStyle = grd;
        ctx.beginPath();
        ctx.arc(mx, my, rad, 0, 7);
        ctx.fill();
        // spiky star
        ctx.strokeStyle = `rgba(255,230,160,${0.8 * g.muzzle})`;
        ctx.lineWidth = 2.5;
        ctx.beginPath();
        for (let i = 0; i < 5; i++) {
          const a = (i / 5) * Math.PI * 2 + t * 31;
          ctx.moveTo(mx, my);
          ctx.lineTo(mx + Math.cos(a) * rad * 0.55, my + Math.sin(a) * rad * 0.55);
        }
        ctx.stroke();
      }
    }

    // --- damage vignette ---
    const lowHp = g.hp > 0 && g.hp <= 30 ? (0.5 + 0.5 * Math.sin(t * 5)) * (1 - g.hp / 30) * 0.45 : 0;
    const dmg = Math.min(1, g.damageFlash + lowHp);
    if (dmg > 0.02) {
      const grd = ctx.createRadialGradient(
        VIEW_W / 2, VIEW_H / 2, VIEW_H * 0.25,
        VIEW_W / 2, VIEW_H / 2, VIEW_H * 0.72,
      );
      grd.addColorStop(0, 'rgba(180,0,0,0)');
      grd.addColorStop(1, `rgba(170,10,5,${0.62 * dmg})`);
      ctx.fillStyle = grd;
      ctx.fillRect(0, 0, VIEW_W, VIEW_H);
    }
    // pickup blink
    if (g.pickupFlash > 0.02) {
      ctx.fillStyle = `rgba(255,250,220,${g.pickupFlash * 0.14})`;
      ctx.fillRect(0, 0, VIEW_W, VIEW_H);
    }

    // permanent corner vignette for mood
    const vin = ctx.createRadialGradient(
      VIEW_W / 2, VIEW_H / 2, VIEW_H * 0.32,
      VIEW_W / 2, VIEW_H / 2, VIEW_H * 0.78,
    );
    vin.addColorStop(0, 'rgba(0,0,0,0)');
    vin.addColorStop(1, 'rgba(0,0,0,0.5)');
    ctx.fillStyle = vin;
    ctx.fillRect(0, 0, VIEW_W, VIEW_H);

    // film grain
    ctx.globalAlpha = 0.5;
    const ox = (Math.random() * 64) | 0, oy = (Math.random() * 64) | 0;
    for (let y = -oy; y < VIEW_H; y += 128) {
      for (let x = -ox; x < VIEW_W; x += 128) {
        ctx.drawImage(this.grain, x, y);
      }
    }
    ctx.globalAlpha = 1;

    // toast
    if (g.toastT > 0 && g.toast) {
      ctx.font = 'bold 11px monospace';
      ctx.textAlign = 'center';
      const a = Math.min(1, g.toastT / 0.4);
      ctx.fillStyle = `rgba(0,0,0,${0.55 * a})`;
      const w = ctx.measureText(g.toast).width + 14;
      ctx.fillRect(VIEW_W / 2 - w / 2, 36, w, 18);
      ctx.fillStyle = `rgba(255,200,120,${a})`;
      ctx.fillText(g.toast, VIEW_W / 2, 49);
    }

    // crosshair
    if (g.phase === 'playing') {
      ctx.strokeStyle = 'rgba(255,235,200,0.55)';
      ctx.lineWidth = 1;
      const cx = VIEW_W / 2, cy = VIEW_H / 2;
      ctx.beginPath();
      ctx.moveTo(cx - 5, cy); ctx.lineTo(cx - 2, cy);
      ctx.moveTo(cx + 2, cy); ctx.lineTo(cx + 5, cy);
      ctx.moveTo(cx, cy - 5); ctx.lineTo(cx, cy - 2);
      ctx.moveTo(cx, cy + 2); ctx.lineTo(cx, cy + 5);
      ctx.stroke();
    }
  }
}
