// main.js — app state, controls, and the WASM bridge wiring for the reikhelm
// browser playground. Loads the real reikhelm-core compiled to WASM, generates
// a dungeon in-browser, and hands the envelope to the renderer.

import init, { generate, default_config_json } from './pkg/reikhelm_wasm.js';
import { renderMap, getStage, THEMES } from './render.js';
import { downloadDepth, exportDepthDataURL, exportDepthToServer } from './depth.js';
import { exportFPVDataURL, exportFPVToServer, vantage, directedVantage } from './fpv.js';
import { dungeonManifest } from './explore.js';

const canvas = document.getElementById('view');
const ctx = canvas.getContext('2d');
const panel = document.getElementById('controls');
const readout = document.getElementById('readout');
const stageWrap = document.getElementById('stage-wrap');
const stageSlider = document.getElementById('stage');
const stageLabel = document.getElementById('stage-label');

// --- app state -----------------------------------------------------------

const state = {
  seed: 1,
  config: null,    // parsed DungeonConfig (nested object)
  env: null,       // last successful envelope
  stage: 0,        // snapshot index currently shown
  followFinal: true,
  opts: { lighting: true, themes: true, entities: true, outlines: false, graph: false, grid: false },
  genMs: 0,
};

// Slider definitions: [config path, label, min, max, step].
const SLIDERS = [
  ['width', 'map width', 32, 160, 1],
  ['height', 'map height', 24, 100, 1],
  ['bsp.max_depth', 'BSP depth', 1, 7, 1],
  ['bsp.min_leaf', 'min leaf', 6, 24, 1],
  ['rooms.min_size', 'room min', 3, 16, 1],
  ['rooms.max_size', 'room max', 4, 22, 1],
  ['rooms.margin', 'margin', 0, 3, 1],
  ['connect.extra_edge_ratio', 'loop edges', 0, 0.8, 0.02],
  ['doors.door_chance', 'door chance', 0, 1, 0.05],
  ['rooms.shapes.rect', '▢ rect', 0, 3, 0.1],
  ['rooms.shapes.octagon', '⬡ octagon', 0, 3, 0.1],
  ['rooms.shapes.ellipse', '◯ ellipse', 0, 3, 0.1],
  ['rooms.shapes.plus', '✚ plus', 0, 3, 0.1],
  ['pillars.room_chance', 'pillars', 0, 1, 0.05],
  ['pools.water_chance', '≈ water', 0, 1, 0.05],
  ['pools.lava_chance', '♨ lava', 0, 1, 0.05],
  ['entities.treasure_chance', '◆ treasure', 0, 1, 0.05],
  ['entities.monster_chance', '● monsters', 0, 1, 0.05],
];

function getPath(obj, path) {
  return path.split('.').reduce((o, k) => o[k], obj);
}
function setPath(obj, path, val) {
  const keys = path.split('.');
  const last = keys.pop();
  keys.reduce((o, k) => o[k], obj)[last] = val;
}

// --- generation + draw ---------------------------------------------------

function regenerate() {
  const t0 = performance.now();
  const json = generate(state.seed, JSON.stringify(state.config));
  state.genMs = performance.now() - t0;
  const env = JSON.parse(json);
  if (!env.ok) {
    readout.querySelector('#err').textContent = '⚠ ' + env.error;
    return; // keep last good render
  }
  readout.querySelector('#err').textContent = '';
  state.env = env;
  const lastStage = (env.snapshots?.length ?? 1) - 1;
  if (state.followFinal) state.stage = lastStage;
  stageSlider.max = String(lastStage);
  stageSlider.value = String(state.stage);
  draw();
  updateReadout();
}

function draw() {
  if (!state.env) return;
  const rect = canvas.getBoundingClientRect();
  renderMap(ctx, rect.width, rect.height, state.env, { ...state.opts, stage: state.stage });
  const st = getStage(state.env, state.stage);
  const last = (state.env.snapshots?.length ?? 1) - 1;
  stageLabel.textContent = `${state.stage + 1}/${last + 1} · ${st.label}${state.stage === last ? ' (final)' : ''}`;
}

function countTiles(env, code) {
  let n = 0;
  for (const row of env.tiles) for (const t of row) if (t === code) n++;
  return n;
}

// Count themed rooms by theme id (final-stage regions carry the theme tag).
function themeCounts(env) {
  const counts = {};
  for (const r of env.regions) {
    if (r.kind === 'Room' && r.theme) counts[r.theme] = (counts[r.theme] || 0) + 1;
  }
  return counts;
}

function updateReadout() {
  const env = state.env;
  const rooms = env.regions.filter((r) => r.kind === 'Room' && r.cells.length > 0).length;
  const corrs = env.regions.filter((r) => r.kind === 'Corridor').length;
  const doors = countTiles(env, 2);
  const ents = env.entities || [];
  const byKind = (k) => ents.filter((e) => e.kind === k).length;
  const counts = themeCounts(env);
  const legend = Object.keys(THEMES)
    .filter((k) => counts[k])
    .map((k) => `${THEMES[k].glyph} ${THEMES[k].label} <b>${counts[k]}</b>`)
    .join(' · ');
  readout.querySelector('#stats').innerHTML =
    `seed <b>${env.seed}</b> · ${env.width}×${env.height} · ` +
    `<b>${rooms}</b> rooms · ${corrs} corridors · ${doors} doors · ` +
    `gen <b>${state.genMs.toFixed(1)}</b> ms<br>` +
    `◆ <b>${byKind('Treasure')}</b> treasure · ● <b>${byKind('Monster')}</b> monsters · ` +
    `${byKind('Entrance')} entrance · ${byKind('Exit')} exit` +
    (legend ? `<br>${legend}` : '');
}

// --- controls UI ---------------------------------------------------------

function buildControls() {
  // Seed row.
  const seedRow = document.createElement('div');
  seedRow.className = 'row seed-row';
  seedRow.innerHTML = `
    <label>seed</label>
    <input type="number" id="seed" min="0" step="1" value="${state.seed}">
    <button id="reroll">⟳ reroll</button>
    <button id="depth" title="export top-down depth map PNG (for the AI-render pipeline)">⬇ depth</button>`;
  panel.appendChild(seedRow);
  seedRow.querySelector('#seed').addEventListener('input', (e) => {
    state.seed = Math.max(0, parseInt(e.target.value || '0', 10));
    regenerate();
  });
  seedRow.querySelector('#reroll').addEventListener('click', () => {
    state.seed = Math.floor(Math.random() * 1e9);
    document.getElementById('seed').value = String(state.seed);
    regenerate();
  });
  seedRow.querySelector('#depth').addEventListener('click', () => {
    if (state.env) downloadDepth(state.env, { stage: state.stage });
  });

  // Sliders.
  for (const [path, label, min, max, step] of SLIDERS) {
    const row = document.createElement('div');
    row.className = 'row';
    const val = getPath(state.config, path);
    row.innerHTML = `
      <label>${label}</label>
      <input type="range" data-path="${path}" min="${min}" max="${max}" step="${step}" value="${val}">
      <span class="val" data-for="${path}">${fmt(val, step)}</span>`;
    panel.appendChild(row);
    const slider = row.querySelector('input');
    slider.addEventListener('input', (e) => {
      const v = step < 1 ? parseFloat(e.target.value) : parseInt(e.target.value, 10);
      setPath(state.config, path, v);
      row.querySelector('.val').textContent = fmt(v, step);
      regenerate();
    });
  }

  // Toggles.
  const togRow = document.createElement('div');
  togRow.className = 'row toggles';
  togRow.innerHTML = ['lighting', 'themes', 'entities', 'outlines', 'graph', 'grid']
    .map((k) => `<label class="tog"><input type="checkbox" data-opt="${k}" ${state.opts[k] ? 'checked' : ''}>${k}</label>`)
    .join('');
  panel.appendChild(togRow);
  togRow.querySelectorAll('input').forEach((cb) => {
    cb.addEventListener('change', (e) => {
      state.opts[e.target.dataset.opt] = e.target.checked;
      draw();
    });
  });
}

function fmt(v, step) { return step < 1 ? v.toFixed(2) : String(v); }

// Stage scrubber.
stageSlider.addEventListener('input', (e) => {
  state.stage = parseInt(e.target.value, 10);
  const last = (state.env?.snapshots?.length ?? 1) - 1;
  state.followFinal = state.stage === last;
  draw();
});

// Keyboard: R reroll, ←/→ scrub stages.
window.addEventListener('keydown', (e) => {
  if (e.target.tagName === 'INPUT') return;
  if (e.key === 'r' || e.key === 'R') document.getElementById('reroll').click();
  if (e.key === 'ArrowLeft') { stageSlider.value = String(Math.max(0, state.stage - 1)); stageSlider.dispatchEvent(new Event('input')); }
  if (e.key === 'ArrowRight') { const last = (state.env?.snapshots?.length ?? 1) - 1; stageSlider.value = String(Math.min(last, state.stage + 1)); stageSlider.dispatchEvent(new Event('input')); }
});

function resize() {
  const dpr = window.devicePixelRatio || 1;
  const rect = canvas.getBoundingClientRect();
  canvas.width = Math.floor(rect.width * dpr);
  canvas.height = Math.floor(rect.height * dpr);
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  draw();
}
window.addEventListener('resize', resize);

// --- automation hooks (used by Playwright-driven iteration) --------------
window.reikhelm = {
  setSeed(s) { state.seed = s | 0; const el = document.getElementById('seed'); if (el) el.value = String(s); regenerate(); },
  setStage(i) { stageSlider.value = String(i); stageSlider.dispatchEvent(new Event('input')); },
  setOpt(k, v) { state.opts[k] = v; const cb = document.querySelector(`[data-opt="${k}"]`); if (cb) cb.checked = v; draw(); },
  setConfig(path, v) { setPath(state.config, path, v); regenerate(); },
  // Depth-map export (drives the AI-render pipeline). exportDepth → data URL +
  // dims (for headless capture); exportDepthToServer → POST the PNG to serve.py.
  exportDepth(opts) { return state.env ? exportDepthDataURL(state.env, opts) : null; },
  downloadDepth(opts) { return state.env ? downloadDepth(state.env, opts) : null; },
  exportDepthToServer(name, opts) { return state.env ? exportDepthToServer(state.env, name, opts) : null; },
  // First-person (Eye-of-the-Beholder style) depth — raycast from a room centre.
  exportFPV(opts) { return state.env ? exportFPVDataURL(state.env, opts) : null; },
  exportFPVToServer(name, opts) { return state.env ? exportFPVToServer(state.env, name, opts) : null; },
  vantage(opts) { return state.env ? vantage(state.env, opts) : null; },
  dungeonManifest() { return state.env ? dungeonManifest(state.env) : null; },
  // Save a JSON blob to the dev server (used to drop an atlas manifest to disk).
  async saveJSON(name, obj) {
    const r = await fetch('/save/' + name, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(obj) });
    if (!r.ok) throw new Error('saveJSON failed: ' + r.status);
    return r.json();
  },
  // Export every room × 4 cardinal FPV depth maps + the manifest for a baked atlas.
  async bakeAtlasDepths(seed) {
    if (seed != null) this.setSeed(seed);
    const m = dungeonManifest(state.env); // nav + open already derived from real tile openings
    const sd = state.env.seed;
    for (const r of m.rooms) {
      for (const dir of ['N', 'E', 'S', 'W']) {
        // Per-direction camera: gap-aligned when the way is open, near-wall
        // when it's closed — so the depth map can't be misread by the model.
        const at = directedVantage(state.env, state.env.regions[r.id], dir);
        await exportFPVToServer(state.env, `atlas/${sd}/_work/r${r.id}_${dir}_depth.png`, { at, dir });
      }
    }
    await this.saveJSON(`atlas/${sd}/manifest.json`, m);
    return { seed: sd, rooms: m.rooms.length, frames: m.rooms.length * 4, themes: m.rooms.map((r) => `${r.id}:${r.theme}`) };
  },
  state: () => ({ seed: state.seed, stage: state.stage, genMs: state.genMs, config: state.config,
    rooms: state.env?.regions.filter((r) => r.kind === 'Room' && r.cells.length > 0).length,
    pillarTiles: state.env ? countTiles(state.env, 3) : 0,
    water: state.env ? countTiles(state.env, 4) : 0,
    lava: state.env ? countTiles(state.env, 5) : 0,
    entities: state.env ? state.env.entities.length : 0,
    themes: state.env ? themeCounts(state.env) : {},
    ok: !!state.env }),
};

// --- boot ----------------------------------------------------------------

async function boot() {
  await init();
  state.config = JSON.parse(default_config_json());
  buildControls();
  resize();           // sizes the canvas and triggers the first draw via regenerate below
  regenerate();
  document.body.dataset.ready = '1';  // a flag Playwright can wait on
}

boot().catch((e) => {
  readout.querySelector('#err').textContent = 'boot failed: ' + e;
  console.error(e);
});
