// depth.js — top-down depth-map exporter for reikhelm map envelopes.
//
// A *renderer* concern, exactly like render.js: it reads the same JSON envelope
// (integer tile codes, regions, themes) and derives an 8-bit grayscale depth map
// for the diffusion control pipeline (tools/comfy-spike → ComfyUI Z-Image). It
// holds NO generation logic and the core stores none of this — height, like
// color, lives renderer-side and is derived from tiles + regions + theme.
//
// Convention (matches tools/comfy-spike/comfy.py): a top-down depth map where
// NEAR the camera = white(255), FAR = black(0). Looking straight down, the tops
// of walls are closest to the camera (white), the floor sits mid-gray, and the
// bottoms of recessed pools are farthest (black). So depth here is a *height
// field*: brightness ∝ height.
//
// Note on the BFS field: render.js's `distanceToWall` is an openness/cavity map,
// not a depth map — feeding it raw would dome the floors toward the camera (room
// centres bulging), which is wrong. We build a height model instead, and reuse
// the distance field only for a subtle ambient-occlusion darkening in the
// crevices where stone meets floor.

import { getStage, distanceToWall } from './render.js';

const WALL = 0, FLOOR = 1, DOOR = 2, PILLAR = 3, WATER = 4, LAVA = 5;

// Per-tile base height in [0,1] (1 = tallest = nearest the top-down camera =
// whitest). The wall plateau is the high reference; pools recess below the
// floor; a door notches just under floor level so passages read as openings;
// a pillar stands as a tall column on the floor. Renderer-owned, like PAL.
const HEIGHT = {
  [WALL]:   1.00, // flat wall-top plateau (uniform: from directly above, all wall tops sit level)
  [FLOOR]:  0.50, // mid reference plane
  [DOOR]:   0.45, // threshold, a hair below floor → reads as a gap in the wall line
  [PILLAR]: 0.84, // free-standing column rising off the floor
  [WATER]:  0.15, // recessed pool
  [LAVA]:   0.10, // recessed molten channel (lowest)
};

// Per-theme relief (Stage-B layer, kept subtle so it modulates the room without
// swamping the wall↔pool contrast that carries the structure). Floor bias raises
// or sinks a themed room's floor; wall bias makes its walls stand a touch taller.
// Keyed by the wire theme id (render.js THEMES). Absent themes → flat (0,0).
const THEME_RELIEF = {
  throne:    { floor:  0.07, wall: 0.06 }, // a raised dais under taller walls
  vault:     { floor:  0.04, wall: 0.03 }, // built-up stone room
  hall:      { floor:  0.025, wall: 0.02 },
  library:   { floor:  0.02, wall: 0.01 },
  forge:     { floor:  0.0,  wall: 0.01 }, // lava already carries the relief
  threshold: { floor:  0.0,  wall: 0.0 },
  stone:     { floor:  0.0,  wall: 0.0 },
  den:       { floor: -0.02, wall: 0.0 },
  cistern:   { floor: -0.06, wall: 0.0 }, // sunken water room
  crypt:     { floor: -0.05, wall: 0.0 }, // sunken vault
};

const AO = { depth: 2.4, strength: 0.07 }; // crevice darkening near walls (reuses distanceToWall)

const clamp = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;

// Per-cell theme relief: for every cell of a themed Room, its {floor,wall} bias;
// {0,0} elsewhere. Rooms don't overlap, so the assignment is unambiguous.
function reliefField(regions, w, h) {
  const fl = new Float32Array(w * h);
  const wl = new Float32Array(w * h);
  for (const r of regions) {
    if (r.kind !== 'Room' || !r.theme || !r.cells) continue;
    const rel = THEME_RELIEF[r.theme];
    if (!rel) continue;
    for (const [x, y] of r.cells) {
      if (x < 0 || y < 0 || x >= w || y >= h) continue;
      fl[y * w + x] = rel.floor;
      wl[y * w + x] = rel.wall;
    }
  }
  return { fl, wl };
}

// In-place separable box blur over a Float32 field (two passes ≈ Gaussian).
// Used as a small bevel so wall edges read as sloped faces rather than 1px
// cliffs, without melting the flat plateaus. radius in pixels; clamps at edges.
function boxBlur(src, w, h, radius, passes = 2) {
  if (radius < 1) return src;
  let buf = src;
  const tmp = new Float32Array(w * h);
  const norm = 1 / (radius * 2 + 1);
  for (let p = 0; p < passes; p++) {
    // horizontal
    for (let y = 0; y < h; y++) {
      const row = y * w;
      let acc = 0;
      for (let k = -radius; k <= radius; k++) acc += buf[row + clamp(k, 0, w - 1)];
      for (let x = 0; x < w; x++) {
        tmp[row + x] = acc * norm;
        const out = row + clamp(x - radius, 0, w - 1);
        const inc = row + clamp(x + radius + 1, 0, w - 1);
        acc += buf[inc] - buf[out];
      }
    }
    // vertical
    for (let x = 0; x < w; x++) {
      let acc = 0;
      for (let k = -radius; k <= radius; k++) acc += tmp[clamp(k, 0, h - 1) * w + x];
      for (let y = 0; y < h; y++) {
        buf[y * w + x] = acc * norm;
        const out = clamp(y - radius, 0, h - 1) * w + x;
        const inc = clamp(y + radius + 1, 0, h - 1) * w + x;
        acc += tmp[inc] - tmp[out];
      }
    }
  }
  return buf;
}

// --- main entry ----------------------------------------------------------

// Derive a depth height field from an envelope. Returns { width, height, gray }
// where gray is a Uint8ClampedArray (row-major, one 8-bit value per pixel),
// full-range normalized with near(tall)=255.
//
// opts: { stage?, scale?, bevel?, ao? }
//   stage  — snapshot to read (default final)
//   scale  — pixels per cell (default ≈ 1024 / longest map edge, clamped 6..48)
//   bevel  — edge-softening as a fraction of a cell (default 0.16; 0 = hard steps)
//   ao     — crevice AO strength (default AO.strength; 0 = off)
export function computeDepthField(env, opts = {}) {
  const { tiles, regions } = getStage(env, opts.stage);
  const w = env.width, h = env.height;
  const scale = opts.scale ?? clamp(Math.round(1024 / Math.max(w, h)), 6, 48);
  const aoStrength = opts.ao ?? AO.strength;

  // 1) Per-cell height = tile base + theme relief − crevice AO.
  const dist = distanceToWall(tiles, w, h);
  const { fl, wl } = reliefField(regions, w, h);
  const cell = new Float32Array(w * h);
  for (let y = 0; y < h; y++) {
    for (let x = 0; x < w; x++) {
      const i = y * w + x;
      const t = tiles[y][x];
      let v = HEIGHT[t] ?? HEIGHT[FLOOR];
      if (t === WALL) {
        v += wl[i]; // taller themed walls
      } else {
        v += fl[i]; // raised/sunken themed floors
        // Crevice AO: open cells right against stone sit slightly lower. `dist`
        // is 0 in walls and grows outward; openness = min(dist/aoDepth, 1).
        const openness = Math.min(1, dist[i] / AO.depth);
        v -= (1 - openness) * aoStrength;
      }
      cell[i] = v;
    }
  }

  // 2) Upscale to pixels as flat blocks (nearest), preserving plateaus/pools —
  //    hard depth steps at wall tops are physically correct top-down.
  const W = w * scale, H = h * scale;
  const px = new Float32Array(W * H);
  for (let y = 0; y < H; y++) {
    const cy = (y / scale) | 0;
    for (let x = 0; x < W; x++) {
      px[y * W + x] = cell[cy * w + ((x / scale) | 0)];
    }
  }

  // 3) Light bevel: a small blur turns the 1px block edges into sloped wall
  //    faces the depth ControlNet can read, without rounding off the flat tops.
  const bevel = opts.bevel ?? 0.16;
  const radius = Math.max(0, Math.round(scale * bevel));
  boxBlur(px, W, H, radius);

  // 4) Height → 8-bit gray, near(tall)=white. Default ('levels') maps heights
  //    onto the *proven* room_depth levels (floor≈120, wall≈235, recessed pools
  //    ≈30–45 — the input distribution the Z-Image depth patch was validated
  //    against): gray = h·230 + 5, so floor(0.5)→120 and wall(1.0)→235. This
  //    keeps the floor a stable mid-gray across seeds instead of letting one tall
  //    themed room crush every other floor toward black (what literal full-range
  //    did). `normalize:'range'` opts back into data-driven full-range.
  const gray = new Uint8ClampedArray(W * H);
  if (opts.normalize === 'range') {
    let lo = Infinity, hi = -Infinity;
    for (let i = 0; i < px.length; i++) { const v = px[i]; if (v < lo) lo = v; if (v > hi) hi = v; }
    const span = hi - lo || 1;
    for (let i = 0; i < px.length; i++) gray[i] = ((px[i] - lo) / span) * 255 + 0.5;
  } else {
    for (let i = 0; i < px.length; i++) gray[i] = px[i] * 230 + 5 + 0.5;
  }

  return { width: W, height: H, gray };
}

// --- canvas / export glue ------------------------------------------------

// Paint a depth field onto a fresh offscreen canvas (R=G=B=gray).
export function depthToCanvas(field) {
  const { width, height, gray } = field;
  const cv = document.createElement('canvas');
  cv.width = width; cv.height = height;
  const c = cv.getContext('2d');
  const img = c.createImageData(width, height);
  const d = img.data;
  for (let i = 0; i < gray.length; i++) {
    const v = gray[i], j = i * 4;
    d[j] = v; d[j + 1] = v; d[j + 2] = v; d[j + 3] = 255;
  }
  c.putImageData(img, 0, 0);
  return cv;
}

// Build the depth PNG and return its data URL plus dimensions.
export function exportDepthDataURL(env, opts = {}) {
  const field = computeDepthField(env, opts);
  return { url: depthToCanvas(field).toDataURL('image/png'), width: field.width, height: field.height };
}

// Trigger a browser download of the depth PNG (the human / button path).
export function downloadDepth(env, opts = {}) {
  const { url, width, height } = exportDepthDataURL(env, opts);
  const a = document.createElement('a');
  a.href = url;
  a.download = `reikhelm-depth-seed${env.seed}-${width}x${height}.png`;
  document.body.appendChild(a);
  a.click();
  a.remove();
  return { width, height };
}

// POST the depth PNG to the dev save-server (tools/serve.py) under `name` — the
// automated-pipeline path (Playwright / scripted export). Returns the server's
// JSON ({ok, path, bytes}) merged with the dimensions; throws if no server is
// listening (use downloadDepth() for the browser-download path instead).
export async function exportDepthToServer(env, name, opts = {}) {
  const field = computeDepthField(env, opts);
  const blob = await new Promise((res) => depthToCanvas(field).toBlob(res, 'image/png'));
  const resp = await fetch('/save/' + encodeURIComponent(name), { method: 'POST', body: blob });
  if (!resp.ok) throw new Error('save failed: ' + resp.status);
  const info = await resp.json();
  return { ...info, width: field.width, height: field.height };
}
