(function () {
  'use strict';

  const LEVEL_FORMAT_VERSION = 1;

  const TILE_NAMES = [
    'Empty',
    'STARTAN',
    'STONE',
    'TECH',
    'DOOR',
    'SWITCH',
    'Secret(Brick)',
    'Secret(Stone)',
    'Secret(Tech)',
    'Key Red',
    'Key Yellow',
    'Key Blue',
  ];
  const DOOR_TILES = [4, 6, 7, 8, 9, 10, 11];
  /** In raycaster preview, player can walk through doors and switches for easier level design. */
  const PREVIEW_PASSTHROUGH_TILES = [4, 5, 6, 7, 8, 9, 10, 11]; // DOOR, SWITCH, secret doors, key doors
  const ENEMY_NAMES = ['Zombie', 'Sergeant', 'Imp', 'Demon', 'Commando'];
  const PICKUP_NAMES = [
    'Ammo clip',
    'Health small',
    'Health large',
    'Shotgun',
    'Helmet',
    'Armor',
    'Shells',
    'Rocket launcher',
    'Key Red',
    'Key Yellow',
    'Key Blue',
    'Chaingun',
  ];

  /** Game limits (must match enemy.h, pickup.h, door.h). Editor enforces these and shows caps in UI. */
  const GAME_LIMITS = {
    MAX_ENEMIES: 21,
    MAX_PICKUPS: 16,
    MAX_DOORS: 24,
    MAX_SWITCHES: 4,
  };

  let state = {
    mapW: 32,
    mapH: 32,
    map: null,
    spawn1: null,
    spawn2: null,
    enemies: [],
    pickups: [],
    switchLinks: [],
    brush: 1,
    placing: null,
    enemyType: 0,
    pickupType: 0,
    hoverCell: null, // { x, y, type, index? } or null
    hoverEntity: null, // { type: 'spawn1'|'spawn2'|'enemy'|'pickup', index } or null
    linkMode: false,
    pendingSwitchIndex: null,
    previewPos: null, // { x, y } in tile space (float) or null to use spawn1
    previewAngle: null, // 0-1023 or null to use spawn1.angle
  };

  function cellIndex(x, y) {
    return y * state.mapW + x;
  }
  function getCell(x, y) {
    return state.map[cellIndex(x, y)];
  }
  function setCell(x, y, v) {
    const isBorder = x === 0 || x === state.mapW - 1 || y === 0 || y === state.mapH - 1;
    if (isBorder && v === 0) v = 1;
    state.map[cellIndex(x, y)] = v;
  }

  function fillBorderWalls() {
    const w = state.mapW,
      h = state.mapH;
    for (let x = 0; x < w; x++) {
      state.map[cellIndex(x, 0)] = 1;
      state.map[cellIndex(x, h - 1)] = 1;
    }
    for (let y = 0; y < h; y++) {
      state.map[cellIndex(0, y)] = 1;
      state.map[cellIndex(w - 1, y)] = 1;
    }
  }

  function resizeMap(w, h) {
    const newMap = new Uint8Array(w * h);
    const copyW = Math.min(w, state.mapW);
    const copyH = Math.min(h, state.mapH);
    for (let y = 0; y < copyH; y++)
      for (let x = 0; x < copyW; x++) newMap[y * w + x] = getCell(x, y);
    state.mapW = w;
    state.mapH = h;
    state.map = newMap;
    state.spawn1 = state.spawn1 && state.spawn1.x < w && state.spawn1.y < h ? state.spawn1 : null;
    state.spawn2 = state.spawn2 && state.spawn2.x < w && state.spawn2.y < h ? state.spawn2 : null;
    state.enemies = state.enemies.filter((e) => e.tileX < w && e.tileY < h);
    state.pickups = state.pickups.filter((p) => p.tileX < w && p.tileY < h);
    state.switchLinks = [];
    fillBorderWalls();
    refreshDoorSwitchLists();
  }

  function getDoors() {
    const list = [];
    for (let y = 0; y < state.mapH; y++)
      for (let x = 0; x < state.mapW; x++)
        if (DOOR_TILES.includes(getCell(x, y)))
          list.push({ tileX: x, tileY: y, tile: getCell(x, y) });
    return list;
  }

  function getSwitches() {
    const list = [];
    for (let y = 0; y < state.mapH; y++)
      for (let x = 0; x < state.mapW; x++)
        if (getCell(x, y) === 5) list.push({ tileX: x, tileY: y });
    return list;
  }

  function getCellInfo(x, y) {
    if (x < 0 || x >= state.mapW || y < 0 || y >= state.mapH) return null;
    if (state.spawn1 && state.spawn1.x === x && state.spawn1.y === y)
      return { type: 'spawn1', index: 0, label: 'Spawn 1' };
    if (state.spawn2 && state.spawn2.x === x && state.spawn2.y === y)
      return { type: 'spawn2', index: 0, label: 'Spawn 2' };
    const ei = state.enemies.findIndex((e) => e.tileX === x && e.tileY === y);
    if (ei >= 0)
      return {
        type: 'enemy',
        index: ei,
        label: ENEMY_NAMES[state.enemies[ei].type] + ' (' + x + ',' + y + ')',
      };
    const pi = state.pickups.findIndex((p) => p.tileX === x && p.tileY === y);
    if (pi >= 0)
      return {
        type: 'pickup',
        index: pi,
        label: PICKUP_NAMES[state.pickups[pi].type] + ' (' + x + ',' + y + ')',
      };
    const doors = getDoors();
    const di = doors.findIndex((d) => d.tileX === x && d.tileY === y);
    if (di >= 0) return { type: 'door', index: di, label: 'Door ' + di };
    const switches = getSwitches();
    const si = switches.findIndex((s) => s.tileX === x && s.tileY === y);
    if (si >= 0) {
      const link = state.switchLinks[si];
      const linkStr = link === -1 ? 'EXIT' : 'Door ' + link;
      return { type: 'switch', index: si, label: 'Switch ' + si + ' → ' + linkStr };
    }
    const t = getCell(x, y);
    return { type: 'empty', index: 0, label: t === 0 ? 'Empty' : TILE_NAMES[t] };
  }

  function refreshDoorSwitchLists() {
    const doors = getDoors();
    const switches = getSwitches();
    while (state.switchLinks.length < switches.length) state.switchLinks.push(0);
    state.switchLinks.length = switches.length;

    const doorList = document.getElementById('doorList');
    const doorCap = GAME_LIMITS.MAX_DOORS;
    const switchCap = GAME_LIMITS.MAX_SWITCHES;
    const doorOver = doors.length > doorCap;
    const switchOver = switches.length > switchCap;
    const doorHeader = document.getElementById('doorListCap');
    if (doorHeader) {
      doorHeader.textContent = 'Doors: ' + doors.length + ' / ' + doorCap;
      doorHeader.classList.toggle('over-limit', doorOver);
    }
    const switchHeader = document.getElementById('switchListCap');
    if (switchHeader) {
      switchHeader.textContent = 'Switches: ' + switches.length + ' / ' + switchCap;
      switchHeader.classList.toggle('over-limit', switchOver);
    }
    doorList.innerHTML = '';
    if (doors.length) {
      doors.forEach((d, i) => {
        const li = document.createElement('li');
        li.textContent = 'Door ' + i + ' (' + d.tileX + ',' + d.tileY + ')';
        doorList.appendChild(li);
      });
    } else {
      doorList.appendChild(document.createElement('li')).textContent = 'None';
    }

    const switchList = document.getElementById('switchList');
    switchList.innerHTML = '';
    if (switches.length) {
      switches.forEach((s, i) => {
        const li = document.createElement('li');
        const sel = document.createElement('select');
        sel.className = 'switch-link';
        sel.innerHTML =
          '<option value="-1">EXIT</option>' +
          doors.map((_, d) => `<option value="${d}">Door ${d}</option>`).join('');
        sel.value = String(state.switchLinks[i] === -1 ? -1 : state.switchLinks[i]);
        sel.addEventListener('change', () => {
          state.switchLinks[i] = parseInt(sel.value, 10);
        });
        li.appendChild(document.createTextNode(`Switch ${i} (${s.tileX},${s.tileY}) → `));
        li.appendChild(sel);
        switchList.appendChild(li);
      });
    } else {
      switchList.appendChild(document.createElement('li')).textContent = 'None';
    }
  }

  function initMap() {
    const size = document.getElementById('mapSize').value.split(',').map(Number);
    const w = size[0],
      h = size[1];
    if (state.map === null) {
      state.map = new Uint8Array(w * h);
      state.mapW = w;
      state.mapH = h;
    } else resizeMap(w, h);
    fillBorderWalls();
    document.getElementById('status').textContent =
      `Map ${state.mapW}×${state.mapH}. Click or drag to paint.`;
    refreshDoorSwitchLists();
    refreshEntityList();
    state.previewPos = state.spawn1
      ? { x: state.spawn1.x + 0.5, y: state.spawn1.y + 0.5 }
      : { x: state.mapW / 2, y: state.mapH / 2 };
    state.previewAngle = state.spawn1 ? (state.spawn1.angle != null ? state.spawn1.angle : 512) : 0;
    drawGrid();
    if (typeof resizePreview === 'function') resizePreview();
  }

  const canvas = document.getElementById('grid');
  const ctx = canvas.getContext('2d');
  const CELL_PX = 12;
  const MAX_CANVAS = 600;

  function drawGrid() {
    const w = state.mapW;
    const h = state.mapH;
    let cellPx = CELL_PX;
    let cw = w * cellPx,
      ch = h * cellPx;
    if (cw > MAX_CANVAS || ch > MAX_CANVAS) {
      cellPx = Math.floor(MAX_CANVAS / Math.max(w, h));
      cw = w * cellPx;
      ch = h * cellPx;
    }
    canvas.width = cw;
    canvas.height = ch;
    canvas.dataset.cellPx = cellPx;

    for (let y = 0; y < h; y++)
      for (let x = 0; x < w; x++) {
        let tile = getCell(x, y);
        ctx.fillStyle =
          tile <= 11
            ? [
                '#1a0a0a',
                '#8b4513',
                '#555',
                '#4a4a6a',
                '#6a3a2a',
                '#5a4a3a',
                '#7a3525',
                '#454545',
                '#3a3a5a',
                '#8b2a2a',
                '#8b7a2a',
                '#2a4a8b',
              ][tile]
            : '#333';
        ctx.fillRect(x * cellPx, y * cellPx, cellPx, cellPx);
        if (DOOR_TILES.includes(tile)) {
          ctx.strokeStyle = '#aaa';
          ctx.lineWidth = 1;
          ctx.strokeRect(x * cellPx, y * cellPx, cellPx, cellPx);
        }
      }
    ctx.strokeStyle = '#333';
    ctx.lineWidth = 1;
    for (let x = 0; x <= w; x++) ctx.strokeRect(x * cellPx, 0, 0, ch);
    for (let y = 0; y <= h; y++) ctx.strokeRect(0, y * cellPx, cw, 0);

    const cx = (x) => (x + 0.5) * cellPx;
    const cy = (y) => (y + 0.5) * cellPx;
    const r = cellPx * 0.4;
    const facingLen = cellPx * 0.48;
    const drawFacing = (centerX, centerY, angle, strokeColor) => {
      const angleRad = (angle / 1024) * 2 * Math.PI - Math.PI / 2;
      ctx.strokeStyle = strokeColor || '#fff';
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.moveTo(centerX, centerY);
      ctx.lineTo(
        centerX + Math.cos(angleRad) * facingLen,
        centerY + Math.sin(angleRad) * facingLen,
      );
      ctx.stroke();
    };
    if (state.spawn1) {
      const x = state.spawn1.x,
        y = state.spawn1.y;
      const angle = state.spawn1.angle != null ? state.spawn1.angle : 512;
      ctx.fillStyle = '#0a3';
      ctx.fillRect(x * cellPx, y * cellPx, cellPx, cellPx);
      ctx.strokeStyle = '#0a5';
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.moveTo(cx(x) - r, cy(y));
      ctx.lineTo(cx(x) + r, cy(y));
      ctx.moveTo(cx(x), cy(y) - r);
      ctx.lineTo(cx(x), cy(y) + r);
      ctx.stroke();
      drawFacing(cx(x), cy(y), angle, '#fff');
    }
    if (state.spawn2) {
      const x = state.spawn2.x,
        y = state.spawn2.y;
      const angle = state.spawn2.angle != null ? state.spawn2.angle : 512;
      ctx.fillStyle = '#03a';
      ctx.fillRect(x * cellPx, y * cellPx, cellPx, cellPx);
      ctx.strokeStyle = '#05a';
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.moveTo(cx(x) - r, cy(y));
      ctx.lineTo(cx(x) + r, cy(y));
      ctx.moveTo(cx(x), cy(y) - r);
      ctx.lineTo(cx(x), cy(y) + r);
      ctx.stroke();
      drawFacing(cx(x), cy(y), angle, '#fff');
    }
    const ENEMY_GRID_COLORS = ['#e05050', '#8b8b00', '#e08020', '#a020a0', '#00cc80'];
    const PICKUP_GRID_COLORS = [
      '#ffcc00',
      '#50ff50',
      '#50ffff',
      '#ff9050',
      '#c0c0c0',
      '#8080ff',
      '#ffa060',
      '#ff4040',
      '#ff2020',  /* Key Red */
      '#ffff20',  /* Key Yellow */
      '#2020ff',  /* Key Blue */
      '#ff8020',  /* Chaingun */
    ];
    state.enemies.forEach((e) => {
      ctx.fillStyle = ENEMY_GRID_COLORS[e.type] || '#e00';
      ctx.beginPath();
      ctx.arc(cx(e.tileX), cy(e.tileY), r, 0, 2 * Math.PI);
      ctx.fill();
      ctx.strokeStyle = '#c00';
      ctx.lineWidth = 1.5;
      ctx.stroke();
      drawFacing(cx(e.tileX), cy(e.tileY), e.angle != null ? e.angle : 0, '#fff');
    });
    state.pickups.forEach((p) => {
      const x = p.tileX,
        y = p.tileY;
      const left = x * cellPx + cellPx * 0.2,
        right = (x + 1) * cellPx - cellPx * 0.2;
      const top = y * cellPx + cellPx * 0.15,
        bottom = (y + 1) * cellPx - cellPx * 0.15;
      ctx.fillStyle = PICKUP_GRID_COLORS[p.type] || '#5a5';
      ctx.beginPath();
      ctx.moveTo(cx(x), top);
      ctx.lineTo(right, bottom);
      ctx.lineTo(left, bottom);
      ctx.closePath();
      ctx.fill();
    });

    if (state.hoverEntity) {
      let hx, hy;
      if (state.hoverEntity.type === 'spawn1' && state.spawn1) {
        hx = state.spawn1.x;
        hy = state.spawn1.y;
      } else if (state.hoverEntity.type === 'spawn2' && state.spawn2) {
        hx = state.spawn2.x;
        hy = state.spawn2.y;
      } else if (state.hoverEntity.type === 'enemy' && state.enemies[state.hoverEntity.index]) {
        const e = state.enemies[state.hoverEntity.index];
        hx = e.tileX;
        hy = e.tileY;
      } else if (state.hoverEntity.type === 'pickup' && state.pickups[state.hoverEntity.index]) {
        const p = state.pickups[state.hoverEntity.index];
        hx = p.tileX;
        hy = p.tileY;
      } else {
        hx = null;
        hy = null;
      }
      if (hx != null && hy != null) {
        ctx.strokeStyle = '#fff';
        ctx.lineWidth = 2;
        ctx.strokeRect(hx * cellPx, hy * cellPx, cellPx, cellPx);
      }
    }
    if (state.linkMode && state.pendingSwitchIndex !== null) {
      const switches = getSwitches();
      const sw = switches[state.pendingSwitchIndex];
      if (sw) {
        ctx.strokeStyle = '#ff0';
        ctx.lineWidth = 2;
        ctx.strokeRect(sw.tileX * cellPx, sw.tileY * cellPx, cellPx, cellPx);
      }
    }

    const overlay = document.getElementById('gridOverlay');
    if (overlay) {
      overlay.width = cw;
      overlay.height = ch;
    }
  }

  function drawPreviewOnMap() {
    const overlay = document.getElementById('gridOverlay');
    if (!overlay || overlay.width === 0 || overlay.height === 0 || !state.map) return;
    const cellPx = Number(canvas.dataset.cellPx) || CELL_PX;
    const preview = getPreviewPlayer();
    const ox = overlay.getContext('2d');
    ox.clearRect(0, 0, overlay.width, overlay.height);
    const px = preview.x * cellPx,
      py = preview.y * cellPx;
    const previewR = Math.max(3, cellPx * 0.2);
    const len = cellPx * 0.5;
    ox.fillStyle = '#ff0';
    ox.beginPath();
    ox.arc(px, py, previewR, 0, 2 * Math.PI);
    ox.fill();
    ox.strokeStyle = '#aa0';
    ox.lineWidth = 1;
    ox.stroke();
    ox.strokeStyle = '#ff0';
    ox.lineWidth = 2;
    const angleRad = (preview.angle / 1024) * 2 * Math.PI - Math.PI / 2;
    ox.beginPath();
    ox.moveTo(px, py);
    ox.lineTo(px + Math.cos(angleRad) * len, py + Math.sin(angleRad) * len);
    ox.stroke();
  }

  function getPreviewPlayer() {
    const x = state.previewPos != null ? state.previewPos.x : state.mapW / 2;
    const y = state.previewPos != null ? state.previewPos.y : state.mapH / 2;
    const angle = state.previewAngle != null ? state.previewAngle : 0;
    return { x, y, angle };
  }

  const WALL_COLORS = [
    '#1a0a0a',
    '#8b4513',
    '#555',
    '#4a4a6a',
    '#6a3a2a',
    '#5a4a3a',
    '#7a3525',
    '#454545',
    '#3a3a5a',
    '#8b2a2a',
    '#8b7a2a',
    '#2a4a8b',
  ];
  const WALL_TEXTURE_URLS = [
    null,
    '../doom_graphics_unvirtualboyed/wall_textures/WALL01_1.png',
    '../doom_graphics_unvirtualboyed/wall_textures/WALL02_1.png',
    '../doom_graphics_unvirtualboyed/wall_textures/WALL03_1.png',
    '../door_texture.png',
    '../gaint-switch1-doom-palette.png',
    '../doom_graphics_unvirtualboyed/wall_textures/WALL01_1.png',
    '../doom_graphics_unvirtualboyed/wall_textures/WALL02_1.png',
    '../doom_graphics_unvirtualboyed/wall_textures/WALL03_1.png',
    '../door_texture.png',
    '../door_texture.png',
    '../door_texture.png',
  ];
  const wallTextures = [];
  (function loadWallTextures() {
    for (let t = 1; t <= 11; t++) {
      const img = new Image();
      img.src = WALL_TEXTURE_URLS[t] || '';
      wallTextures[t] = img;
    }
  })();

  const ENEMY_SPRITE_URLS = [
    '../doom_graphics_unvirtualboyed/monster_imp/TROOA1.png',
    '../doom_graphics_unvirtualboyed/monster_imp/TROOA2A8.png',
    '../doom_graphics_unvirtualboyed/monster_imp/TROOA3A7.png',
    '../doom_graphics_unvirtualboyed/monster_imp/TROOA4A6.png',
    '../doom_graphics_unvirtualboyed/monster_imp/TROOA5.png',
  ];
  const enemySprites = [];
  (function loadEnemySprites() {
    ENEMY_SPRITE_URLS.forEach((url, i) => {
      const img = new Image();
      img.src = url;
      enemySprites[i] = img;
    });
  })();
  const spritePlayer = new Image();
  spritePlayer.src = '../doom_graphics_unvirtualboyed/player/PLAYA1.png';
  const spritePickup = new Image();
  spritePickup.src = '../doom_graphics_unvirtualboyed/stage_pickups_n_shit/AMMOA0.png';
  const spritePistol = new Image();
  spritePistol.src = '../doom_graphics_unvirtualboyed/pistol/PISGA0.png';

  function getEnemySpriteIndex(ent, px, py) {
    // Sprite sheet uses angle from ENEMY TO PLAYER; atan2 gives player→enemy, so add π
    const dx = ent.x - px,
      dy = ent.y - py;
    let angle = Math.atan2(dx, -dy) + Math.PI;
    if (angle < 0) angle += 2 * Math.PI;
    let segment = Math.floor((angle / (2 * Math.PI)) * 8) % 8;
    // Offset by enemy's facing so the dropdown direction updates the raycaster
    const enemy = state.enemies[ent.i];
    const faceAngle = enemy.angle != null ? enemy.angle : 0;
    segment = (segment + Math.floor(faceAngle / 128)) % 8;
    return segment;
  }

  function drawRaycastPreview(ctx, width, height) {
    const player = getPreviewPlayer();
    const px = player.x,
      py = player.y;
    const angleRad = (player.angle / 1024) * 2 * Math.PI - Math.PI / 2;
    const fov = Math.PI / 3;
    const halfFov = fov / 2;
    const numRays = width;
    const W = state.mapW,
      H = state.mapH;

    ctx.fillStyle = '#1a1a2a';
    ctx.fillRect(0, 0, width, height / 2);
    ctx.fillStyle = '#2a1a1a';
    ctx.fillRect(0, height / 2, width, height / 2);

    const step = 0.02;
    for (let col = 0; col < numRays; col++) {
      const t = col / numRays - 0.5;
      const rayAngle = angleRad + t * fov;
      const dx = Math.cos(rayAngle),
        dy = Math.sin(rayAngle);
      let x = px,
        y = py;
      let dist = 0;
      let hit = 0;
      let hitX = 0,
        hitY = 0;
      for (let i = 0; i < 400; i++) {
        const gx = Math.floor(x),
          gy = Math.floor(y);
        if (gx < 0 || gx >= W || gy < 0 || gy >= H) break;
        const cell = getCell(gx, gy);
        if (cell > 0 && !PREVIEW_PASSTHROUGH_TILES.includes(cell)) {
          hit = cell;
          hitX = x;
          hitY = y;
          break;
        }
        x += dx * step;
        y += dy * step;
        dist += step;
      }
      if (hit > 0 && hit <= 11) {
        const gx = Math.floor(hitX),
          gy = Math.floor(hitY);
        const fracX = hitX - gx,
          fracY = hitY - gy;
        const minDist = Math.min(fracX, 1 - fracX, fracY, 1 - fracY);
        const texU = minDist === fracX || minDist === 1 - fracX ? fracY : fracX;
        const wallHeight = Math.min(height * 1.2, height / (dist + 0.01));
        const top = (height - wallHeight) / 2;
        const shade = Math.min(1, 1 / (dist * 0.3));
        const img = wallTextures[hit];
        if (img && img.complete && img.naturalWidth) {
          const tw = img.naturalWidth,
            th = img.naturalHeight;
          const srcX = Math.floor(texU * tw) % tw;
          ctx.globalAlpha = shade;
          ctx.drawImage(img, srcX, 0, 1, th, col, top, 1, wallHeight);
          ctx.globalAlpha = 1;
        } else {
          ctx.fillStyle = WALL_COLORS[hit];
          ctx.globalAlpha = shade;
          ctx.fillRect(col, top, 1, wallHeight);
          ctx.globalAlpha = 1;
        }
      }
    }

    const viewAngle = (player.angle / 1024) * 2 * Math.PI;
    const halfFovRad = fov / 2;
    const proj = width / 2 / Math.tan(halfFovRad);
    const SPRITE_WORLD_SIZE = 0.8;

    const ENEMY_COLORS = ['#e05050', '#8b8b00', '#e08020', '#a020a0', '#00cc80'];
    const PICKUP_COLORS = [
      '#ffcc00',
      '#50ff50',
      '#50ffff',
      '#ff9050',
      '#c0c0c0',
      '#8080ff',
      '#ffa060',
      '#ff4040',
      '#ff2020',  /* Key Red */
      '#ffff20',  /* Key Yellow */
      '#2020ff',  /* Key Blue */
      '#ff8020',  /* Chaingun */
    ];
    const entities = [];
    state.enemies.forEach((e, i) =>
      entities.push({ x: e.tileX + 0.5, y: e.tileY + 0.5, type: 'enemy', i, enemyType: e.type }),
    );
    state.pickups.forEach((p, i) =>
      entities.push({ x: p.tileX + 0.5, y: p.tileY + 0.5, type: 'pickup', i, pickupType: p.type }),
    );
    if (state.spawn1 && (Math.floor(px) !== state.spawn1.x || Math.floor(py) !== state.spawn1.y))
      entities.push({ x: state.spawn1.x + 0.5, y: state.spawn1.y + 0.5, type: 'spawn', id: 1 });
    if (state.spawn2)
      entities.push({ x: state.spawn2.x + 0.5, y: state.spawn2.y + 0.5, type: 'spawn', id: 2 });
    entities.forEach((ent) => {
      const dx = ent.x - px,
        dy = ent.y - py;
      const dist = Math.sqrt(dx * dx + dy * dy);
      if (dist < 0.05) return;
      const angleTo = Math.atan2(dx, -dy);
      let angleDiff = angleTo - viewAngle;
      while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
      while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
      if (angleDiff < -halfFovRad || angleDiff > halfFovRad) return;
      ent.depth = dist * Math.cos(angleDiff);
      ent.angleDiff = angleDiff;
      ent.dist = dist;
    });
    const visible = entities.filter((e) => e.depth != null);

    // Raycast from player to each entity; skip if blocked by wall (reuses step from above)
    const raycastTo = (ex, ey) => {
      const rdx = ex - px,
        rdy = ey - py;
      const rdist = Math.sqrt(rdx * rdx + rdy * rdy);
      if (rdist < 0.05) return true;
      const rnx = (rdx / rdist) * step,
        rny = (rdy / rdist) * step;
      let rx = px,
        ry = py;
      const steps = Math.ceil(rdist / step);
      for (let i = 0; i < steps; i++) {
        rx += rnx;
        ry += rny;
        const gx = Math.floor(rx),
          gy = Math.floor(ry);
        if (gx < 0 || gx >= W || gy < 0 || gy >= H) return false;
        const c = getCell(gx, gy);
        if (c > 0 && !PREVIEW_PASSTHROUGH_TILES.includes(c)) return false; // hit solid wall before entity
      }
      return true;
    };
    const reallyVisible = visible.filter((ent) => raycastTo(ent.x, ent.y));
    reallyVisible.sort((a, b) => b.depth - a.depth);

    const ENEMY_SPRITE_MAP = [0, 1, 2, 3, 4, 3, 2, 1];
    const ENEMY_SPRITE_FLIP = [false, false, false, false, false, true, true, true];
    reallyVisible.forEach((ent) => {
      const dist = ent.dist;
      const angleDiff = ent.angleDiff;
      const depth = ent.depth;
      const screenX = (width / 2) * (1 + Math.tan(angleDiff) / Math.tan(halfFovRad));
      const h = Math.max(8, Math.min(height * 0.9, (SPRITE_WORLD_SIZE * proj) / depth));
      const w = h;
      const left = screenX - w / 2;
      const top = (height - h) / 2;
      if (left + w < 0 || left > width) return;
      ctx.globalAlpha = 1; // full opacity for sprites (was distance shading)

      if (ent.type === 'enemy') {
        const segment = getEnemySpriteIndex(ent, px, py);
        const imgIdx = ENEMY_SPRITE_MAP[segment];
        const img = enemySprites[imgIdx];
        if (img && img.complete && img.naturalWidth) {
          const w = (img.naturalWidth / img.naturalHeight) * h;
          const x = screenX - w / 2;
          if (ENEMY_SPRITE_FLIP[segment]) {
            ctx.save();
            ctx.translate(screenX, 0);
            ctx.scale(-1, 1);
            ctx.translate(-screenX, 0);
            ctx.drawImage(img, x, top, w, h);
            ctx.restore();
          } else {
            ctx.drawImage(img, x, top, w, h);
          }
        } else {
          const c = ENEMY_COLORS[ent.enemyType] || '#e00';
          ctx.fillStyle = c;
          ctx.fillRect(screenX - 4, top, 8, h);
        }
      } else if (ent.type === 'pickup') {
        const img = spritePickup;
        if (img && img.complete && img.naturalWidth) {
          const w = (img.naturalWidth / img.naturalHeight) * h;
          ctx.drawImage(img, screenX - w / 2, top, w, h);
        } else {
          const c = PICKUP_COLORS[ent.pickupType] || '#5a5';
          ctx.fillStyle = c;
          ctx.fillRect(screenX - 4, top, 8, h);
        }
      } else {
        const img = spritePlayer;
        if (img && img.complete && img.naturalWidth) {
          const w = (img.naturalWidth / img.naturalHeight) * h;
          ctx.drawImage(img, screenX - w / 2, top, w, h);
        } else {
          ctx.fillStyle = '#0a5';
          ctx.fillRect(screenX - 4, top, 8, h);
          ctx.fillStyle = '#fff';
          ctx.font = '10px sans-serif';
          ctx.fillText(String(ent.id || ''), screenX - 3, top + 10);
        }
      }
      ctx.globalAlpha = 1;
    });
    ctx.globalAlpha = 1;

    if (spritePistol.complete && spritePistol.naturalWidth) {
      const pw = spritePistol.naturalWidth,
        ph = spritePistol.naturalHeight;
      const scale = Math.min(width / pw, (height * 0.4) / ph);
      const w = pw * scale,
        h = ph * scale;
      ctx.drawImage(spritePistol, (width - w) / 2, height - h, w, h);
    }
  }

  function updateTooltip(clientX, clientY, info) {
    const el = document.getElementById('gridTooltip');
    if (!info) {
      el.classList.add('hidden');
      el.textContent = '';
      return;
    }
    el.textContent = info.label;
    el.classList.remove('hidden');
    el.style.left = clientX + 10 + 'px';
    el.style.top = clientY + 10 + 'px';
  }

  const ANGLE_OPTIONS = [
    [0, 'N'],
    [128, 'NE'],
    [256, 'E'],
    [384, 'SE'],
    [512, 'S'],
    [640, 'SW'],
    [768, 'W'],
    [896, 'NW'],
  ];
  function refreshEntityList() {
    const ul = document.getElementById('entityList');
    ul.innerHTML = '';
    const add = (entityType, index, label, getAngle, setAngle) => {
      const li = document.createElement('li');
      li.setAttribute('data-entity', entityType);
      li.setAttribute('data-index', String(index));
      li.appendChild(document.createTextNode(label));
      if (getAngle != null && setAngle != null) {
        const sel = document.createElement('select');
        sel.className = 'entity-angle';
        sel.title = 'Direction';
        ANGLE_OPTIONS.forEach(([val, dir]) => {
          const opt = document.createElement('option');
          opt.value = String(val);
          opt.textContent = dir;
          sel.appendChild(opt);
        });
        const current = getAngle();
        const closest = ANGLE_OPTIONS.reduce((a, b) =>
          Math.abs(a[0] - current) <= Math.abs(b[0] - current) ? a : b,
        );
        sel.value = String(closest[0]);
        sel.addEventListener('change', () => {
          setAngle(parseInt(sel.value, 10));
          drawGrid();
        });
        li.appendChild(document.createTextNode(' '));
        li.appendChild(sel);
      }
      li.addEventListener('mouseenter', () => {
        state.hoverEntity = { type: entityType, index };
        drawGrid();
      });
      li.addEventListener('mouseleave', () => {
        state.hoverEntity = null;
        drawGrid();
      });
      ul.appendChild(li);
    };
    if (state.spawn1)
      add(
        'spawn1',
        0,
        'Spawn 1 (' + state.spawn1.x + ',' + state.spawn1.y + ')',
        () => (state.spawn1.angle != null ? state.spawn1.angle : 512),
        (v) => {
          state.spawn1.angle = v;
        },
      );
    const enemyCap = GAME_LIMITS.MAX_ENEMIES;
    const pickupCap = GAME_LIMITS.MAX_PICKUPS;
    const entityCapEl = document.getElementById('entityListCap');
    if (entityCapEl) {
      entityCapEl.textContent =
        'Enemies: ' +
        state.enemies.length +
        ' / ' +
        enemyCap +
        '  ·  Pickups: ' +
        state.pickups.length +
        ' / ' +
        pickupCap;
      entityCapEl.classList.toggle(
        'over-limit',
        state.enemies.length > enemyCap || state.pickups.length > pickupCap,
      );
    }
    if (state.spawn2)
      add(
        'spawn2',
        0,
        'Spawn 2 (' + state.spawn2.x + ',' + state.spawn2.y + ')',
        () => (state.spawn2.angle != null ? state.spawn2.angle : 512),
        (v) => {
          state.spawn2.angle = v;
        },
      );
    state.enemies.forEach((e, i) =>
      add(
        'enemy',
        i,
        ENEMY_NAMES[e.type] + ' (' + e.tileX + ',' + e.tileY + ')',
        () => (state.enemies[i].angle != null ? state.enemies[i].angle : 0),
        (v) => {
          state.enemies[i].angle = v;
        },
      ),
    );
    state.pickups.forEach((p, i) =>
      add('pickup', i, PICKUP_NAMES[p.type] + ' (' + p.tileX + ',' + p.tileY + ')', null, null),
    );
    if (!ul.children.length) {
      const li = document.createElement('li');
      li.textContent = 'None';
      ul.appendChild(li);
    }
    updateEntityListHighlight();
  }

  /** Show a short-lived message (toast) so user sees limits / errors without blocking. */
  let toastTimeout = null;
  function showToast(message, durationMs) {
    if (durationMs == null) durationMs = 2800;
    const el = document.getElementById('toast');
    if (!el) return;
    el.textContent = message;
    el.classList.remove('hidden');
    if (toastTimeout) clearTimeout(toastTimeout);
    toastTimeout = setTimeout(() => {
      el.classList.add('hidden');
      toastTimeout = null;
    }, durationMs);
  }

  function updateEntityListHighlight() {
    document.querySelectorAll('#entityList [data-entity]').forEach((li) => {
      const type = li.getAttribute('data-entity');
      const index = parseInt(li.getAttribute('data-index'), 10);
      const match =
        state.hoverCell && state.hoverCell.type === type && state.hoverCell.index === index;
      li.classList.toggle('highlight', !!match);
    });
  }

  function xyFromEvent(e) {
    const rect = canvas.getBoundingClientRect();
    const cellPx = Number(canvas.dataset.cellPx) || CELL_PX;
    const x = Math.floor((e.clientX - rect.left) / cellPx);
    const y = Math.floor((e.clientY - rect.top) / cellPx);
    return { x, y };
  }

  function onGridClick(e) {
    const { x, y } = xyFromEvent(e);
    if (x < 0 || x >= state.mapW || y < 0 || y >= state.mapH) return;

    if (state.linkMode) {
      const switches = getSwitches();
      const doors = getDoors();
      const switchIdx = switches.findIndex((s) => s.tileX === x && s.tileY === y);
      const doorIdx = doors.findIndex((d) => d.tileX === x && d.tileY === y);
      if (state.pendingSwitchIndex === null) {
        if (switchIdx >= 0) {
          state.pendingSwitchIndex = switchIdx;
          document.getElementById('pendingSwitchNum').textContent = String(switchIdx);
          document.getElementById('linkModeExit').classList.remove('hidden');
          document.getElementById('status').textContent =
            'Switch ' + switchIdx + ' selected. Click a door to link, or set EXIT below.';
          drawGrid();
        }
        return;
      }
      if (doorIdx >= 0) {
        state.switchLinks[state.pendingSwitchIndex] = doorIdx;
        state.pendingSwitchIndex = null;
        document.getElementById('linkModeExit').classList.add('hidden');
        refreshDoorSwitchLists();
        document.getElementById('status').textContent =
          'Map ' + state.mapW + '×' + state.mapH + '. Click or drag to paint.';
        drawGrid();
        return;
      }
      if (switchIdx >= 0) {
        state.pendingSwitchIndex = switchIdx;
        document.getElementById('pendingSwitchNum').textContent = String(switchIdx);
        document.getElementById('linkModeExit').classList.remove('hidden');
        document.getElementById('status').textContent =
          'Switch ' + switchIdx + ' selected. Click a door to link, or set EXIT below.';
        drawGrid();
        return;
      }
      state.pendingSwitchIndex = null;
      document.getElementById('linkModeExit').classList.add('hidden');
      document.getElementById('status').textContent =
        'Map ' + state.mapW + '×' + state.mapH + '. Click or drag to paint.';
      drawGrid();
      return;
    }

    if (state.placing === 'spawn1') {
      state.spawn1 = { x, y, angle: 512 };
      state.placing = null;
      document.getElementById('brushSpawn1').classList.remove('selected');
    } else if (state.placing === 'spawn2') {
      state.spawn2 = { x, y, angle: 512 };
      state.placing = null;
      document.getElementById('brushSpawn2').classList.remove('selected');
    } else if (state.placing === 'enemy') {
      if (state.enemies.length >= GAME_LIMITS.MAX_ENEMIES) {
        showToast(
          'Max enemies reached (' +
            GAME_LIMITS.MAX_ENEMIES +
            '). Remove one or increase MAX_ENEMIES in enemy.h.',
        );
        return;
      }
      state.enemies.push({ type: state.enemyType, tileX: x, tileY: y, angle: 0 });
      state.placing = null;
      document.getElementById('brushEnemy').value = '';
    } else if (state.placing === 'pickup') {
      if (state.pickups.length >= GAME_LIMITS.MAX_PICKUPS) {
        showToast(
          'Max pickups reached (' +
            GAME_LIMITS.MAX_PICKUPS +
            '). Remove one or increase MAX_PICKUPS in pickup.h.',
        );
        return;
      }
      state.pickups.push({ type: state.pickupType, tileX: x, tileY: y });
      state.placing = null;
      document.getElementById('brushPickup').value = '';
    } else {
      setCell(x, y, state.brush);
      refreshDoorSwitchLists();
      const doors = getDoors();
      const switches = getSwitches();
      if (doors.length > GAME_LIMITS.MAX_DOORS)
        showToast(
          'Too many door tiles (' +
            doors.length +
            '). Game max is ' +
            GAME_LIMITS.MAX_DOORS +
            ' (MAX_DOORS in door.h).',
        );
      if (switches.length > GAME_LIMITS.MAX_SWITCHES)
        showToast(
          'Too many switch tiles (' +
            switches.length +
            '). Game max is ' +
            GAME_LIMITS.MAX_SWITCHES +
            ' (MAX_SWITCHES in door.h).',
        );
    }
    refreshEntityList();
    drawGrid();
  }

  function buildJson() {
    return {
      version: LEVEL_FORMAT_VERSION,
      mapW: state.mapW,
      mapH: state.mapH,
      map: Array.from(state.map),
      spawn1: state.spawn1,
      spawn2: state.spawn2,
      enemies: state.enemies.map((e) => ({
        type: e.type,
        tileX: e.tileX,
        tileY: e.tileY,
        angle: e.angle != null ? e.angle : 0,
      })),
      pickups: state.pickups.map((p) => ({ type: p.type, tileX: p.tileX, tileY: p.tileY })),
      switchLinks: state.switchLinks.slice(),
    };
  }

  function loadFromJson(obj) {
    const v = obj.version;
    if (v != null && v > LEVEL_FORMAT_VERSION) {
      alert('Level was saved with a newer editor. Please update the editor.');
      return false;
    }
    const mapW = obj.mapW,
      mapH = obj.mapH,
      mapArr = obj.map;
    if (
      typeof mapW !== 'number' ||
      typeof mapH !== 'number' ||
      !Array.isArray(mapArr) ||
      mapArr.length !== mapW * mapH
    ) {
      alert('Invalid level JSON: bad map dimensions or map array.');
      return false;
    }
    state.mapW = mapW;
    state.mapH = mapH;
    state.map = new Uint8Array(mapArr);
    state.spawn1 =
      obj.spawn1 && typeof obj.spawn1.x === 'number' && typeof obj.spawn1.y === 'number'
        ? {
            x: obj.spawn1.x,
            y: obj.spawn1.y,
            angle: obj.spawn1.angle != null ? obj.spawn1.angle : 512,
          }
        : null;
    state.spawn2 =
      obj.spawn2 && typeof obj.spawn2.x === 'number' && typeof obj.spawn2.y === 'number'
        ? {
            x: obj.spawn2.x,
            y: obj.spawn2.y,
            angle: obj.spawn2.angle != null ? obj.spawn2.angle : 512,
          }
        : null;
    const rawEnemies = (obj.enemies || []).map((e) => ({
      type: Number(e.type) || 0,
      tileX: Number(e.tileX) || 0,
      tileY: Number(e.tileY) || 0,
      angle: e.angle != null ? Number(e.angle) : 0,
    }));
    const rawPickups = (obj.pickups || []).map((p) => ({
      type: Number(p.type) || 0,
      tileX: Number(p.tileX) || 0,
      tileY: Number(p.tileY) || 0,
    }));
    state.enemies = rawEnemies.slice(0, GAME_LIMITS.MAX_ENEMIES);
    state.pickups = rawPickups.slice(0, GAME_LIMITS.MAX_PICKUPS);
    if (rawEnemies.length > GAME_LIMITS.MAX_ENEMIES)
      showToast(
        'Loaded level had ' +
          rawEnemies.length +
          ' enemies; trimmed to ' +
          GAME_LIMITS.MAX_ENEMIES +
          ' (game limit).',
      );
    if (rawPickups.length > GAME_LIMITS.MAX_PICKUPS)
      showToast(
        'Loaded level had ' +
          rawPickups.length +
          ' pickups; trimmed to ' +
          GAME_LIMITS.MAX_PICKUPS +
          ' (game limit).',
      );
    state.switchLinks = Array.isArray(obj.switchLinks) ? obj.switchLinks.slice() : [];
    state.previewPos = state.spawn1
      ? { x: state.spawn1.x + 0.5, y: state.spawn1.y + 0.5 }
      : { x: state.mapW / 2, y: state.mapH / 2 };
    state.previewAngle = state.spawn1 ? (state.spawn1.angle != null ? state.spawn1.angle : 512) : 0;
    refreshDoorSwitchLists();
    refreshEntityList();
    drawGrid();
    document.getElementById('status').textContent =
      'Map ' + state.mapW + '×' + state.mapH + '. Click or drag to paint.';
    return true;
  }

  /** Content for the .h file only (map + spawn defines). Do NOT put init functions here - they go in enemy.c/pickup.c. */
  function buildExportHeaderFileContent() {
    const id = (document.getElementById('levelId').value || 'e1m4')
      .toLowerCase()
      .replace(/\s/g, '');
    const prefix = id.toUpperCase();
    const W = state.mapW,
      H = state.mapH;
    const cells = W * H;
    let out = '';
    out += `/* VBDOOM_LEVEL_FORMAT ${LEVEL_FORMAT_VERSION} */\n`;
    out += `/* ${id} map: ${W}x${H} */\n`;
    out += `const u8 ${id}_map[${cells}] = {\n`;
    for (let y = 0; y < H; y++) {
      out += '/* Row ' + y + ' */ ';
      for (let x = 0; x < W; x++) out += getCell(x, y) + (x < W - 1 ? ',' : '');
      out += y < H - 1 ? ',\n' : '\n';
    }
    out += '};\n\n';
    const s1 = state.spawn1 || { x: 0, y: 0, angle: 512 };
    const s2 = state.spawn2 || { x: 0, y: 0, angle: 512 };
    out += `/* ${id} spawn and level data */\n`;
    out += `#define ${prefix}_SPAWN_X  (${s1.x} * 256 + 128)\n`;
    out += `#define ${prefix}_SPAWN_Y  (${s1.y} * 256 + 128)\n`;
    out += `#define ${prefix}_SPAWN2_X (${s2.x} * 256 + 128)\n`;
    out += `#define ${prefix}_SPAWN2_Y (${s2.y} * 256 + 128)\n`;
    return out;
  }

  /**
   * Build export sections. Returns an array of {title, code} objects.
   * Each section represents a distinct file or function that the user must
   * copy into the right place.
   */
  function buildExportSections(headerOnly) {
    const id = (document.getElementById('levelId').value || 'e1m4')
      .toLowerCase()
      .replace(/\s/g, '');
    const prefix = id.toUpperCase();
    const W = state.mapW,
      H = state.mapH;
    const cells = W * H;
    const doors = getDoors();
    const switches = getSwitches();
    const s1 = state.spawn1 || { x: 0, y: 0, angle: 512 };
    const s2 = state.spawn2 || { x: 0, y: 0, angle: 512 };
    const sections = [];

    /* ---- Section: header file (map array + defines) ---- */
    {
      let code = `/* VBDOOM_LEVEL_FORMAT ${LEVEL_FORMAT_VERSION} */\n`;
      code += `/* ${id} map: ${W}x${H} */\n`;
      code += `const u8 ${id}_map[${cells}] = {\n`;
      for (let y = 0; y < H; y++) {
        code += '/* Row ' + y + ' */ ';
        for (let x = 0; x < W; x++) code += getCell(x, y) + (x < W - 1 ? ',' : '');
        code += y < H - 1 ? ',\n' : '\n';
      }
      code += '};\n\n';
      code += `#define ${prefix}_SPAWN_X  (${s1.x} * 256 + 128)\n`;
      code += `#define ${prefix}_SPAWN_Y  (${s1.y} * 256 + 128)\n`;
      code += `#define ${prefix}_SPAWN2_X (${s2.x} * 256 + 128)\n`;
      code += `#define ${prefix}_SPAWN2_Y (${s2.y} * 256 + 128)\n`;
      sections.push({
        title: `src/vbdoom/assets/doom/${id}.h  (save as file)`,
        code,
      });
    }

    if (headerOnly) return sections;

    /* ---- Section: RayCasterData.h include ---- */
    sections.push({
      title: 'RayCasterData.h  (add this include)',
      code: `#include "../assets/doom/${id}.h"`,
    });

    /* ---- Section: enemy.c function ---- */
    {
      let code = `void initEnemies${prefix}(void) {\n`;
      code +=
        '\tint i; for (i = 0; i < MAX_ENEMIES; i++) { g_enemies[i].active = false; g_enemies[i].enemyType = ETYPE_ZOMBIEMAN; }\n';
      state.enemies.forEach((e, i) => {
        const health =
          e.type === 0
            ? 'ZOMBIE_HEALTH'
            : e.type === 1
              ? 'SGT_HEALTH'
              : e.type === 2
                ? 'IMP_HEALTH'
                : e.type === 3
                  ? 'DEMON_HEALTH'
                  : 'COMMANDO_HEALTH';
        const angle = e.angle != null ? e.angle : 0;
        code += `\tg_enemies[${i}].x = ${e.tileX} * 256 + 128;\n`;
        code += `\tg_enemies[${i}].y = ${e.tileY} * 256 + 128;\n`;
        code += `\tg_enemies[${i}].angle = ${angle};\n`;
        code += `\tg_enemies[${i}].active = true;\n`;
        code += `\tg_enemies[${i}].enemyType = ETYPE_${['ZOMBIEMAN', 'SERGEANT', 'IMP', 'DEMON', 'COMMANDO'][e.type]};\n`;
        code += `\tg_enemies[${i}].health = ${health};\n`;
      });
      code += '}';
      sections.push({
        title: `enemy.c  (paste this function)`,
        code,
      });
    }

    /* ---- Section: enemy.h declaration ---- */
    sections.push({
      title: 'enemy.h  (add this declaration)',
      code: `void initEnemies${prefix}(void);`,
    });

    /* ---- Section: pickup.c function ---- */
    {
      let code = `void initPickups${prefix}(void) {\n`;
      code += '\tint i; for (i = 0; i < MAX_PICKUPS; i++) g_pickups[i].active = false;\n';
      state.pickups.forEach((p, i) => {
        code += `\tg_pickups[${i}].x = ${p.tileX} * 256 + 128;\n`;
        code += `\tg_pickups[${i}].y = ${p.tileY} * 256 + 128;\n`;
        code += `\tg_pickups[${i}].type = PICKUP_${['AMMO_CLIP', 'HEALTH_SMALL', 'HEALTH_LARGE', 'WEAPON_SHOTGUN', 'HELMET', 'ARMOR', 'SHELLS', 'WEAPON_ROCKET', 'KEY_RED', 'KEY_YELLOW', 'KEY_BLUE', 'WEAPON_CHAINGUN'][p.type]};\n`;
        code += `\tg_pickups[${i}].active = true;\n`;
      });
      code += '}';
      sections.push({
        title: `pickup.c  (paste this function)`,
        code,
      });
    }

    /* ---- Section: pickup.h declaration ---- */
    sections.push({
      title: 'pickup.h  (add this declaration)',
      code: `void initPickups${prefix}(void);`,
    });

    /* ---- Section: gameLoop.c loadLevel() block ---- */
    {
      let code = '';
      code += `{ u16 row; for (row = 0; row < ${H}; row++) copymem((u8*)g_map + row * MAP_X, (const u8*)${id}_map + row * ${W}, ${W}); }\n`;
      code += `{ u16 i; for (i = ${H} * MAP_X; i < MAP_CELLS; i++) ((u8*)g_map)[i] = 0; }\n`;
      code += `{ u16 row; for (row = 0; row < ${H}; row++) { u16 c; for (c = ${W}; c < MAP_X; c++) ((u8*)g_map)[row * MAP_X + c] = 0; } }\n`;
      code += `fPlayerX = ${prefix}_SPAWN_X;\n`;
      code += `fPlayerY = ${prefix}_SPAWN_Y;\n`;
      code += `fPlayerAng = ${s1.angle};\n`;
      code += `initEnemies${prefix}();\n`;
      code += `initPickups${prefix}();\n`;
      code += `initDoors();\n`;
      doors.forEach((d, i) => {
        code += `registerDoor(${d.tileX}, ${d.tileY});  /* door ${i} */\n`;
      });
      switches.forEach((s, i) => {
        const link = state.switchLinks[i];
        const type = link === -1 ? 'SW_EXIT' : 'SW_DOOR';
        const linkArg = link === -1 ? '0' : String(link);
        code += `registerSwitch(${s.tileX}, ${s.tileY}, ${type}, ${linkArg});\n`;
      });
      sections.push({
        title: `gameLoop.c  →  loadLevel()  →  else if (levelNum == N) { ... }`,
        code,
      });
    }

    return sections;
  }

  /** Render an array of {title, code} sections into the export modal container. */
  function renderExportSections(sections) {
    const container = document.getElementById('exportSections');
    container.innerHTML = '';
    sections.forEach((sec, idx) => {
      const wrapper = document.createElement('div');
      wrapper.className = 'export-section';

      const header = document.createElement('div');
      header.className = 'export-section-header';

      const label = document.createElement('span');
      label.className = 'export-section-title';
      label.textContent = sec.title;

      const copyBtn = document.createElement('button');
      copyBtn.type = 'button';
      copyBtn.className = 'export-copy-btn';
      copyBtn.textContent = 'COPY';
      copyBtn.addEventListener('click', () => {
        navigator.clipboard.writeText(sec.code).then(() => {
          copyBtn.textContent = 'Copied!';
          copyBtn.classList.add('copied');
          setTimeout(() => {
            copyBtn.textContent = 'COPY';
            copyBtn.classList.remove('copied');
          }, 1500);
        });
      });

      header.appendChild(label);
      header.appendChild(copyBtn);
      wrapper.appendChild(header);

      const ta = document.createElement('textarea');
      ta.className = 'export-section-textarea';
      ta.readOnly = true;
      ta.value = sec.code;
      /* Auto-size: clamp between 2 and 20 visible lines */
      const lineCount = sec.code.split('\n').length;
      ta.rows = Math.max(2, Math.min(20, lineCount + 1));
      wrapper.appendChild(ta);

      container.appendChild(wrapper);
    });
  }

  function renderBrushGrid() {
    const grid = document.getElementById('brushGrid');
    grid.innerHTML = '';
    for (let t = 0; t <= 11; t++) {
      const cell = document.createElement('div');
      cell.className = 'brush-cell' + (state.brush === t ? ' selected' : '');
      cell.textContent = t;
      cell.title = TILE_NAMES[t];
      cell.style.background = [
        '#1a0a0a',
        '#8b4513',
        '#555',
        '#4a4a6a',
        '#6a3a2a',
        '#5a4a3a',
        '#7a3525',
        '#454545',
        '#3a3a5a',
        '#8b2a2a',
        '#8b7a2a',
        '#2a4a8b',
      ][t];
      cell.addEventListener('click', () => {
        state.brush = t;
        state.placing = null;
        document.querySelectorAll('.brush-cell').forEach((c) => c.classList.remove('selected'));
        document.getElementById('brushSpawn1').classList.remove('selected');
        document.getElementById('brushSpawn2').classList.remove('selected');
        document.getElementById('brushEnemy').value = '';
        document.getElementById('brushPickup').value = '';
        cell.classList.add('selected');
      });
      grid.appendChild(cell);
    }
  }

  document.getElementById('mapSize').addEventListener('change', initMap);
  document.getElementById('levelId').addEventListener('change', () => refreshDoorSwitchLists());

  document.getElementById('brushSpawn1').addEventListener('click', () => {
    state.placing = 'spawn1';
    document.getElementById('brushSpawn2').classList.remove('selected');
    document.getElementById('brushEnemy').value = '';
    document.getElementById('brushPickup').value = '';
    document.getElementById('brushSpawn1').classList.add('selected');
  });
  document.getElementById('brushSpawn2').addEventListener('click', () => {
    state.placing = 'spawn2';
    document.getElementById('brushSpawn1').classList.remove('selected');
    document.getElementById('brushEnemy').value = '';
    document.getElementById('brushPickup').value = '';
    document.getElementById('brushSpawn2').classList.add('selected');
  });
  document.getElementById('brushEnemy').addEventListener('change', function () {
    const v = this.value;
    if (v === '') {
      state.placing = null;
      return;
    }
    if (state.enemies.length >= GAME_LIMITS.MAX_ENEMIES) {
      showToast(
        'Max enemies (' + GAME_LIMITS.MAX_ENEMIES + ') reached. Remove one before adding more.',
      );
      this.value = '';
      return;
    }
    state.placing = 'enemy';
    state.enemyType = parseInt(v, 10);
    document.getElementById('brushSpawn1').classList.remove('selected');
    document.getElementById('brushSpawn2').classList.remove('selected');
    document.getElementById('brushPickup').value = '';
  });
  document.getElementById('brushPickup').addEventListener('change', function () {
    const v = this.value;
    if (v === '') {
      state.placing = null;
      return;
    }
    if (state.pickups.length >= GAME_LIMITS.MAX_PICKUPS) {
      showToast(
        'Max pickups (' + GAME_LIMITS.MAX_PICKUPS + ') reached. Remove one before adding more.',
      );
      this.value = '';
      return;
    }
    state.placing = 'pickup';
    state.pickupType = parseInt(v, 10);
    document.getElementById('brushSpawn1').classList.remove('selected');
    document.getElementById('brushSpawn2').classList.remove('selected');
    document.getElementById('brushEnemy').value = '';
  });

  canvas.addEventListener('click', onGridClick);
  canvas.addEventListener('mousemove', function (e) {
    const { x, y } = xyFromEvent(e);
    const info = x >= 0 && x < state.mapW && y >= 0 && y < state.mapH ? getCellInfo(x, y) : null;
    state.hoverCell = info ? { x, y, type: info.type, index: info.index } : null;
    updateTooltip(e.clientX, e.clientY, info);
    updateEntityListHighlight();
    drawGrid();
  });
  canvas.addEventListener('mouseleave', function () {
    state.hoverCell = null;
    updateTooltip(null, null, null);
    updateEntityListHighlight();
    drawGrid();
  });
  canvas.addEventListener('contextmenu', (e) => e.preventDefault());
  canvas.addEventListener('mousedown', function (e) {
    if (e.button === 2) {
      const { x, y } = xyFromEvent(e);
      if (x < 0 || x >= state.mapW || y < 0 || y >= state.mapH) return;
      if (state.spawn1 && state.spawn1.x === x && state.spawn1.y === y) {
        state.spawn1 = null;
        refreshEntityList();
        drawGrid();
        return;
      }
      if (state.spawn2 && state.spawn2.x === x && state.spawn2.y === y) {
        state.spawn2 = null;
        refreshEntityList();
        drawGrid();
        return;
      }
      const ei = state.enemies.findIndex((en) => en.tileX === x && en.tileY === y);
      if (ei >= 0) {
        state.enemies.splice(ei, 1);
        refreshEntityList();
        drawGrid();
        return;
      }
      const pi = state.pickups.findIndex((p) => p.tileX === x && p.tileY === y);
      if (pi >= 0) {
        state.pickups.splice(pi, 1);
        refreshEntityList();
        drawGrid();
        return;
      }
      return;
    }
    if (e.button !== 0) return;
    let last = xyFromEvent(e);
    const paint = function (moveE) {
      const { x, y } = xyFromEvent(moveE);
      if (
        x >= 0 &&
        x < state.mapW &&
        y >= 0 &&
        y < state.mapH &&
        (x !== last.x || y !== last.y) &&
        !state.placing
      ) {
        setCell(x, y, state.brush);
        refreshDoorSwitchLists();
        drawGrid();
        last = { x, y };
      }
    };
    const up = () => {
      canvas.removeEventListener('mousemove', paint);
      document.removeEventListener('mouseup', up);
    };
    canvas.addEventListener('mousemove', paint);
    document.addEventListener('mouseup', up);
  });

  document.getElementById('btnSaveJson').addEventListener('click', () => {
    const json = JSON.stringify(buildJson(), null, 2);
    const blob = new Blob([json], { type: 'application/json' });
    const a = document.createElement('a');
    a.href = URL.createObjectURL(blob);
    a.download = 'level.json';
    a.click();
    URL.revokeObjectURL(a.href);
  });

  document.getElementById('btnLoadJson').addEventListener('click', () => {
    document.getElementById('loadJsonText').value = '';
    document.getElementById('loadJsonModal').classList.remove('hidden');
  });
  document.getElementById('btnLoadJsonChooseFile').addEventListener('click', () => {
    document.getElementById('loadJsonInput').click();
  });
  document.getElementById('loadJsonInput').addEventListener('change', function () {
    const f = this.files && this.files[0];
    if (!f) return;
    const r = new FileReader();
    r.onload = () => {
      document.getElementById('loadJsonText').value = r.result;
    };
    r.readAsText(f);
    this.value = '';
  });
  document.getElementById('btnLoadJsonApply').addEventListener('click', () => {
    try {
      const text = document.getElementById('loadJsonText').value.trim();
      if (!text) {
        alert('Paste JSON or choose a file.');
        return;
      }
      const obj = JSON.parse(text);
      if (loadFromJson(obj)) document.getElementById('loadJsonModal').classList.add('hidden');
    } catch (err) {
      alert('Invalid JSON: ' + err.message);
    }
  });
  document.getElementById('btnCloseLoadJson').addEventListener('click', () => {
    document.getElementById('loadJsonModal').classList.add('hidden');
  });

  /* ---- Load .h (C header) map import ---- */

  /**
   * Parse a C header file containing a VB Doom map array and spawn defines.
   * Returns { levelId, mapW, mapH, map: number[], spawn1, spawn2 } or null on failure.
   */
  function parseHFile(text) {
    /* 1. Find the map array: const u8 <name>_map[<cells>] = { ... }; */
    const arrMatch = text.match(/const\s+u8\s+(\w+)_map\s*\[\s*(\d+)\s*\]\s*=\s*\{/);
    if (!arrMatch) return null;
    const levelId = arrMatch[1]; /* e.g. "e1m4" */
    const declaredCells = parseInt(arrMatch[2], 10);

    /* Extract body between the opening { and closing }; */
    const openIdx = text.indexOf('{', arrMatch.index);
    const closeIdx = text.indexOf('};', openIdx);
    if (openIdx < 0 || closeIdx < 0) return null;
    const body = text.substring(openIdx + 1, closeIdx);

    /* Parse all integers from the body (skip comments and whitespace) */
    const values = [];
    const numRe = /\b(\d+)\b/g;
    let m;
    /* Strip C comments first so " Row 12 , row numbers aren't counted as tile values */
    const stripped = body.replace(/\/\*[\s\S]*?\*\//g, '');
    while ((m = numRe.exec(stripped)) !== null) {
      values.push(parseInt(m[1], 10));
    }

    if (values.length === 0) return null;
    if (values.length !== declaredCells) {
      /* Accept anyway if close, but warn */
      console.warn(
        'Header declared ' + declaredCells + ' cells but found ' + values.length + ' values.',
      );
    }

    /* 2. Determine dimensions */
    let mapW = 0,
      mapH = 0;
    /* Try the comment: "e1m4 map: 32x32" */
    const dimComment = text.match(/map:\s*(\d+)\s*x\s*(\d+)/i);
    if (dimComment) {
      mapW = parseInt(dimComment[1], 10);
      mapH = parseInt(dimComment[2], 10);
    }
    /* Fall back: count "Row N" comments to get height, derive width */
    if (!mapW || !mapH) {
      const rowComments = body.match(/\/\*\s*Row\s+\d+/g);
      if (rowComments && rowComments.length > 0) {
        mapH = rowComments.length;
        mapW = Math.round(values.length / mapH);
      }
    }
    /* Last resort: assume square */
    if (!mapW || !mapH || mapW * mapH !== values.length) {
      const sq = Math.sqrt(values.length);
      if (sq === Math.floor(sq)) {
        mapW = sq;
        mapH = sq;
      } else {
        /* Try common non-square sizes */
        const tryWidths = [64, 48, 32];
        for (let i = 0; i < tryWidths.length; i++) {
          if (values.length % tryWidths[i] === 0) {
            mapW = tryWidths[i];
            mapH = values.length / tryWidths[i];
            break;
          }
        }
      }
    }
    if (!mapW || !mapH || mapW * mapH !== values.length) return null;

    /* 3. Parse spawn defines: #define <PREFIX>_SPAWN_X (tile * 256 + 128) */
    const prefix = levelId.toUpperCase();
    function parseSpawnCoord(suffix) {
      const re = new RegExp(
        '#define\\s+' +
          prefix +
          '_' +
          suffix +
          '\\s+\\(\\s*(\\d+)\\s*\\*\\s*256\\s*\\+\\s*128\\s*\\)',
      );
      const sm = text.match(re);
      return sm ? parseInt(sm[1], 10) : null;
    }
    const s1x = parseSpawnCoord('SPAWN_X');
    const s1y = parseSpawnCoord('SPAWN_Y');
    const s2x = parseSpawnCoord('SPAWN2_X');
    const s2y = parseSpawnCoord('SPAWN2_Y');

    const spawn1 = s1x !== null && s1y !== null ? { x: s1x, y: s1y, angle: 512 } : null;
    const spawn2 = s2x !== null && s2y !== null ? { x: s2x, y: s2y, angle: 512 } : null;

    return {
      levelId: levelId,
      mapW: mapW,
      mapH: mapH,
      map: values,
      spawn1: spawn1,
      spawn2: spawn2,
    };
  }

  document.getElementById('btnLoadH').addEventListener('click', () => {
    document.getElementById('loadHInput').click();
  });
  document.getElementById('loadHInput').addEventListener('change', function () {
    const f = this.files && this.files[0];
    if (!f) return;
    const r = new FileReader();
    r.onload = function () {
      const parsed = parseHFile(r.result);
      if (!parsed) {
        alert(
          'Could not find a valid map array in the .h file.\nExpected: const u8 <name>_map[N] = { ... };',
        );
        return;
      }
      /* Build a JSON-like object and load via loadFromJson */
      const obj = {
        version: LEVEL_FORMAT_VERSION,
        mapW: parsed.mapW,
        mapH: parsed.mapH,
        map: parsed.map,
        spawn1: parsed.spawn1,
        spawn2: parsed.spawn2,
        enemies: [],
        pickups: [],
        switchLinks: [],
      };
      if (loadFromJson(obj)) {
        /* Set the level ID field to match the loaded map */
        document.getElementById('levelId').value = parsed.levelId;
        /* Update map size dropdown if it matches a known size */
        const sizeKey = parsed.mapW + ',' + parsed.mapH;
        const sizeSelect = document.getElementById('mapSize');
        for (let i = 0; i < sizeSelect.options.length; i++) {
          if (sizeSelect.options[i].value === sizeKey) {
            sizeSelect.value = sizeKey;
            break;
          }
        }
        showToast(
          'Loaded ' +
            parsed.levelId +
            ' (' +
            parsed.mapW +
            'x' +
            parsed.mapH +
            ') from header file. Enemies/pickups not included in .h files — add them manually or load a JSON.',
        );
      }
    };
    r.readAsText(f);
    this.value = '';
  });

  document.getElementById('btnHelp').addEventListener('click', () => {
    document.getElementById('helpModal').classList.remove('hidden');
  });
  document.getElementById('btnCloseHelp').addEventListener('click', () => {
    document.getElementById('helpModal').classList.add('hidden');
  });

  function getExportFileName() {
    const raw = (document.getElementById('levelId').value || 'e1m4').trim();
    const safe =
      raw
        .toLowerCase()
        .replace(/\s+/g, '')
        .replace(/[^a-z0-9_]/g, '') || 'map';
    return safe + '.h';
  }

  function openExportModal() {
    const fn = getExportFileName();
    document.getElementById('exportFileName').textContent = fn;
    const checklistEl = document.getElementById('exportFileNameChecklist');
    if (checklistEl) checklistEl.textContent = fn;
  }
  document.getElementById('btnExport').addEventListener('click', () => {
    renderExportSections(buildExportSections(false));
    openExportModal();
    document.getElementById('exportModal').classList.remove('hidden');
  });
  document.getElementById('btnExportHeader').addEventListener('click', () => {
    renderExportSections(buildExportSections(true));
    openExportModal();
    document.getElementById('exportModal').classList.remove('hidden');
  });
  document.getElementById('btnCloseExport').addEventListener('click', () => {
    document.getElementById('exportModal').classList.add('hidden');
  });

  document.getElementById('btnSaveAsHeader').addEventListener('click', () => {
    const fileName = getExportFileName();
    /* Save only map + spawn defines so the .h can be #included from RayCasterData.h without pulling in enemy/pickup types */
    const text = buildExportHeaderFileContent();
    const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = fileName;
    a.click();
    URL.revokeObjectURL(url);
  });

  document.getElementById('btnLinkMode').addEventListener('click', function () {
    state.linkMode = !state.linkMode;
    this.classList.toggle('selected', state.linkMode);
    if (!state.linkMode) {
      state.pendingSwitchIndex = null;
      document.getElementById('linkModeExit').classList.add('hidden');
      document.getElementById('status').textContent =
        'Map ' + state.mapW + '×' + state.mapH + '. Click or drag to paint.';
    }
    drawGrid();
  });

  document.getElementById('btnSetExit').addEventListener('click', () => {
    if (state.pendingSwitchIndex === null) return;
    state.switchLinks[state.pendingSwitchIndex] = -1;
    state.pendingSwitchIndex = null;
    refreshDoorSwitchLists();
    document.getElementById('linkModeExit').classList.add('hidden');
    document.getElementById('status').textContent =
      'Map ' + state.mapW + '×' + state.mapH + '. Click or drag to paint.';
    drawGrid();
  });

  const previewCanvas = document.getElementById('previewCanvas');
  const previewCtx = previewCanvas.getContext('2d');
  const PREVIEW_HEIGHT = 200;
  function resizePreview() {
    const cw = canvas.width || state.mapW * CELL_PX;
    const w = Math.min(cw, MAX_CANVAS);
    previewCanvas.width = w;
    previewCanvas.height = PREVIEW_HEIGHT;
    if (state.map) drawRaycastPreview(previewCtx, previewCanvas.width, previewCanvas.height);
  }
  const keysPressed = {};
  const PREVIEW_MOVE_SPEED = 0.08;
  const PREVIEW_TURN_SPEED = 4;
  function isPreviewKey(key) {
    return (
      key === 'ArrowUp' ||
      key === 'ArrowDown' ||
      key === 'ArrowLeft' ||
      key === 'ArrowRight' ||
      key === 'w' ||
      key === 'W' ||
      key === 's' ||
      key === 'S' ||
      key === 'a' ||
      key === 'A' ||
      key === 'd' ||
      key === 'D'
    );
  }
  function previewCellBlocked(x, y) {
    const gx = Math.floor(x),
      gy = Math.floor(y);
    if (gx < 0 || gx >= state.mapW || gy < 0 || gy >= state.mapH) return true;
    const tile = getCell(gx, gy);
    if (tile === 0) return false;
    if (PREVIEW_PASSTHROUGH_TILES.includes(tile)) return false; // walk through doors/switches in preview
    return true;
  }
  function applyPreviewInput() {
    if (document.activeElement !== previewCanvas) return;
    const fwd = keysPressed['ArrowUp'] || keysPressed['w'] || keysPressed['W'];
    const back = keysPressed['ArrowDown'] || keysPressed['s'] || keysPressed['S'];
    const left = keysPressed['ArrowLeft'] || keysPressed['a'] || keysPressed['A'];
    const right = keysPressed['ArrowRight'] || keysPressed['d'] || keysPressed['D'];
    if (!fwd && !back && !left && !right) return;
    const player = getPreviewPlayer();
    let nx = player.x,
      ny = player.y;
    let nangle = player.angle;
    const angleRad = (nangle / 1024) * 2 * Math.PI - Math.PI / 2;
    if (fwd) {
      nx += Math.cos(angleRad) * PREVIEW_MOVE_SPEED;
      ny += Math.sin(angleRad) * PREVIEW_MOVE_SPEED;
    }
    if (back) {
      nx -= Math.cos(angleRad) * PREVIEW_MOVE_SPEED;
      ny -= Math.sin(angleRad) * PREVIEW_MOVE_SPEED;
    }
    if (left) nangle = (nangle - PREVIEW_TURN_SPEED + 1024) % 1024;
    if (right) nangle = (nangle + PREVIEW_TURN_SPEED) % 1024;
    let finalX = nx,
      finalY = ny;
    if (previewCellBlocked(nx, ny)) {
      const slideX = !previewCellBlocked(nx, player.y);
      const slideY = !previewCellBlocked(player.x, ny);
      if (slideX && slideY) {
        finalX = nx;
        finalY = player.y;
      } else if (slideX) {
        finalX = nx;
        finalY = player.y;
      } else if (slideY) {
        finalX = player.x;
        finalY = ny;
      } else {
        finalX = player.x;
        finalY = player.y;
      }
    }
    state.previewPos = { x: finalX, y: finalY };
    state.previewAngle = nangle;
  }

  function tickPreview() {
    applyPreviewInput();
    if (previewCanvas.width && previewCanvas.height && state.map)
      drawRaycastPreview(previewCtx, previewCanvas.width, previewCanvas.height);
    drawPreviewOnMap();
    requestAnimationFrame(tickPreview);
  }
  tickPreview();

  window.addEventListener('keydown', function (e) {
    if (!isPreviewKey(e.key)) return;
    if (document.activeElement === previewCanvas) e.preventDefault();
    keysPressed[e.key] = true;
  });
  window.addEventListener('keyup', function (e) {
    if (isPreviewKey(e.key)) keysPressed[e.key] = false;
  });
  previewCanvas.addEventListener('blur', function () {
    Object.keys(keysPressed).forEach((k) => {
      keysPressed[k] = false;
    });
  });

  document.getElementById('btnResetPreview').addEventListener('click', () => {
    state.previewPos = state.spawn1
      ? { x: state.spawn1.x + 0.5, y: state.spawn1.y + 0.5 }
      : { x: state.mapW / 2, y: state.mapH / 2 };
    state.previewAngle = state.spawn1 ? (state.spawn1.angle != null ? state.spawn1.angle : 512) : 0;
  });

  window.addEventListener('resize', resizePreview);
  initMap();
  renderBrushGrid();
  resizePreview();
})();
