// gen_maps.mjs : builds all 10 Tiled maps as .tmx (embedded tileset), for export via Tiled CLI.
// Original layouts, no external tilesets. Run with node; then Tiled exports each .tmx -> .json.
import { mkdirSync, writeFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dir = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dir, '..');
const TMX_DIR = resolve(__dir, 'tmx');
const PNG = resolve(ROOT, 'public/assets/sprites/tileset.png');
mkdirSync(TMX_DIR, { recursive: true });

// tile index (0-based); GID = idx+1
const G = (i) => i + 1;
const t = {
  grass: G(0), grassF: G(1), path: G(2), pathEdge: G(3), water: G(4), waterL: G(5),
  flowerBush: G(6), berry: G(7), treeTop: G(8), treeTrunk: G(9), wall: G(10), roof: G(11),
  door: G(12), fence: G(13), flowerbed: G(14), well: G(15), fog: G(16), board: G(17),
  mailbox: G(18), picnic: G(19), bench: G(20), lamp: G(21), bushS: G(22), sand: G(23),
  wfloor: G(24), wfloorB: G(25), iwall: G(26), iwallTop: G(27), bed: G(28), calendar: G(29),
  counter: G(30), oven: G(31), recipe: G(32), shelf: G(33), shelfItem: G(34), table: G(35),
  chair: G(36), clinicForm: G(37), windowI: G(38), doorI: G(39), wwall: G(40), wfloorW: G(41),
  wfloorWb: G(42), hbed: G(43), wdoor: G(44), wwindow: G(45), ribbon: G(46), stage: G(47),
  flowersRed: G(48), flowersBlue: G(49), mushroom: G(50), rock: G(51), stump: G(52),
  lilypad: G(53), reed: G(54), honey: G(55), crate: G(56), barrel: G(57), bookshelf: G(58),
  clock: G(59), painting: G(60), plantPot: G(61), rug: G(62), darkGrass: G(63), grassF2: G(64),
  waterDeep: G(65), pathCorner: G(66), roofEdge: G(67), houseWindow: G(68), star: G(69),
  snowdrop: G(70), cobble: G(71),
};

function M(w, h, base) {
  return {
    w, h,
    ground: new Array(w * h).fill(base),
    obs: new Array(w * h).fill(0),
    objects: [],
    _oid: 1,
  };
}
const idx = (m, c, r) => r * m.w + c;
function setG(m, c, r, g) { if (c >= 0 && c < m.w && r >= 0 && r < m.h) m.ground[idx(m, c, r)] = g; }
function setO(m, c, r, g) { if (c >= 0 && c < m.w && r >= 0 && r < m.h) m.obs[idx(m, c, r)] = g; }
function rectG(m, c, r, w, h, g) { for (let j = 0; j < h; j++) for (let i = 0; i < w; i++) setG(m, c + i, r + j, g); }
function rectO(m, c, r, w, h, g) { for (let j = 0; j < h; j++) for (let i = 0; i < w; i++) setO(m, c + i, r + j, g); }
function borderO(m, g) {
  for (let c = 0; c < m.w; c++) { setO(m, c, 0, g); setO(m, c, m.h - 1, g); }
  for (let r = 0; r < m.h; r++) { setO(m, 0, r, g); setO(m, m.w - 1, r, g); }
}
function obj(m, name, type, c, r, props = {}, wTiles = 1, hTiles = 1) {
  m.objects.push({ id: m._oid++, name, type, x: c * 16, y: r * 16, w: wTiles * 16, h: hTiles * 16, props });
}

// ---- interior room helper (wood floor + walls) ----
function room(m) {
  rectG(m, 0, 0, m.w, m.h, t.wfloor);
  for (let c = 0; c < m.w; c++) { setO(m, c, 0, t.iwallTop); setO(m, c, m.h - 1, t.iwall); }
  for (let r = 0; r < m.h; r++) { setO(m, 0, r, t.iwall); setO(m, m.w - 1, r, t.iwall); }
}
function exitDoor(m, c, r, target, dest) { setG(m, c, r, t.doorI); setO(m, c, r, 0); obj(m, 'door_' + target, 'door', c, r, { target, dest }); }

const maps = {};

// ============ 1. HOME 12x8 ============
{
  const m = M(12, 8, t.wfloor); room(m);
  setG(m, 5, 1, t.rug); setG(m, 6, 1, t.rug);
  setO(m, 1, 1, t.bed); setO(m, 1, 2, t.bed);       // bed (2 tiles)
  setG(m, 2, 0, t.calendar);                          // 床头日历 on top wall
  setO(m, 9, 1, t.bookshelf); setO(m, 8, 1, t.plantPot);
  setO(m, 8, 5, t.table); setO(m, 9, 5, t.chair);
  setG(m, 4, 0, t.windowI); setG(m, 7, 0, t.windowI);
  exitDoor(m, 6, 7, 'plaza', 'sp_from_home');
  obj(m, 'start', 'spawn', 3, 3);
  obj(m, 'sp_from_plaza', 'spawn', 6, 6);
  obj(m, 'i_bed', 'interact', 2, 2, { kind: 'bed' });
  obj(m, 'i_calendar', 'interact', 2, 1, { kind: 'calendar' });
  maps.home = m;
}

// ============ 2. PLAZA 30x20 ============
{
  const m = M(30, 20, t.grass);
  // scatter grass flowers
  for (let r = 0; r < 20; r++) for (let c = 0; c < 30; c++) if (((c * 5 + r * 7) % 11) === 0) setG(m, c, r, t.grassF);
  borderO(m, t.fence);
  // trees along corners
  for (const [c, r] of [[1, 1], [2, 1], [27, 1], [28, 1], [1, 18], [28, 18]]) { setO(m, c, r, t.treeTrunk); setG(m, c, r - 1 < 0 ? 0 : c, r); }
  // ---- houses along top (home/bakery/grocery/clinic) ----
  function house(c, target, dest) {
    rectO(m, c, 1, 3, 2, t.roof);
    rectO(m, c, 3, 3, 1, t.wall);
    setO(m, c, 2, t.wall); setO(m, c + 2, 2, t.wall);
    setG(m, c + 1, 3, t.door); setO(m, c + 1, 3, 0);
    obj(m, 'door_' + target, 'door', c + 1, 3, { target, dest });
    // path down from door
    setG(m, c + 1, 4, t.path); setG(m, c + 1, 5, t.path);
    obj(m, 'sp_from_' + target, 'spawn', c + 1, 5);
  }
  house(2, 'home', 'sp_from_plaza');
  house(8, 'bakery', 'sp_from_plaza');
  house(15, 'grocery', 'sp_from_plaza');
  house(22, 'clinic', 'sp_from_plaza');
  // central cobble plaza + main path
  rectG(m, 3, 9, 24, 3, t.path);
  rectG(m, 11, 13, 8, 4, t.cobble);
  // festival stage (center) — walkable cobble around; stage tile as decor obstacle at back
  rectO(m, 12, 12, 4, 1, t.stage);
  // bulletin board (afu)
  setO(m, 14, 8, t.board);
  // flowerbed (T2 water) cluster left-center
  setO(m, 5, 13, t.flowerbed); setO(m, 6, 13, t.flowerbed); setO(m, 7, 13, t.flowerbed);
  // twin-bird tree (right)
  setO(m, 24, 5, t.treeTrunk); setG(m, 23, 4, t.treeTop); setG(m, 24, 4, t.treeTop); setG(m, 25, 4, t.treeTop); setG(m, 24, 3, t.treeTop);
  // mailbox (dabao) bottom-center
  setO(m, 15, 17, t.mailbox);
  obj(m, 'i_mailbox', 'interact', 15, 16, { kind: 'mailbox' }); // C3 散落信件
  // decorative
  setO(m, 3, 16, t.bushS); setO(m, 26, 16, t.bushS); setO(m, 9, 8, t.lamp); setO(m, 20, 8, t.lamp);
  // edge exits (leave gaps in fence)
  setO(m, 0, 10, 0); exitDoor(m, 0, 10, 'field', 'sp_from_plaza'); setG(m, 1, 10, t.path); obj(m, 'sp_from_field', 'spawn', 2, 10);
  setO(m, 29, 10, 0); exitDoor(m, 29, 10, 'lake', 'sp_from_plaza'); setG(m, 28, 10, t.path); obj(m, 'sp_from_lake', 'spawn', 27, 10);
  setO(m, 4, 19, 0); exitDoor(m, 4, 19, 'well', 'sp_from_plaza'); setG(m, 4, 18, t.path); obj(m, 'sp_from_well', 'spawn', 4, 17);
  setO(m, 25, 19, 0); exitDoor(m, 25, 19, 'fog', 'sp_from_plaza'); setG(m, 25, 18, t.path); obj(m, 'sp_from_fog', 'spawn', 25, 17);
  // NPCs
  obj(m, 'afu', 'npc', 14, 9, { npc: 'afu' });
  obj(m, 'dabao', 'npc', 15, 18, { npc: 'dabao' });
  obj(m, 'bird1', 'npc', 23, 5, { npc: 'bird1' });
  obj(m, 'bird2', 'npc', 25, 5, { npc: 'bird2' });
  obj(m, 'extra1', 'npc', 10, 15, { npc: 'extra1' });
  obj(m, 'extra2', 'npc', 19, 16, { npc: 'extra2' });
  obj(m, 'extra3', 'npc', 6, 6, { npc: 'extra3' });
  // interacts
  obj(m, 'i_board', 'interact', 14, 9, { kind: 'board' });
  obj(m, 'i_flowerbed', 'interact', 6, 14, { kind: 'flowerbed' });
  obj(m, 'i_festival', 'interact', 14, 14, { kind: 'festival' });
  obj(m, 'i_ribbon0', 'interact', 12, 13, { kind: 'ribbon', n: 0 });
  obj(m, 'i_ribbon1', 'interact', 14, 13, { kind: 'ribbon', n: 1 });
  obj(m, 'i_ribbon2', 'interact', 16, 13, { kind: 'ribbon', n: 2 });
  obj(m, 'i_ribbon3', 'interact', 18, 13, { kind: 'ribbon', n: 3 });
  maps.plaza = m;
}

// ============ 3. BAKERY 15x10 ============
{
  const m = M(15, 10, t.wfloor); room(m);
  rectG(m, 1, 1, 13, 8, t.wfloorB);
  rectO(m, 2, 1, 5, 1, t.oven);
  rectO(m, 1, 6, 8, 1, t.counter);
  setG(m, 4, 5, t.recipe);                    // 配方纸 (C2 on back)
  setO(m, 11, 1, t.shelf); setO(m, 12, 1, t.shelf);
  setO(m, 12, 7, t.barrel); setO(m, 13, 4, t.plantPot);
  setG(m, 8, 0, t.windowI);
  exitDoor(m, 7, 9, 'plaza', 'sp_from_bakery');
  obj(m, 'sp_from_plaza', 'spawn', 7, 8);
  obj(m, 'yuan', 'npc', 4, 7, { npc: 'yuan' });
  obj(m, 'i_recipe', 'interact', 4, 6, { kind: 'recipe' });
  maps.bakery = m;
}

// ============ 4. GROCERY 15x10 ============
{
  const m = M(15, 10, t.wfloor); room(m);
  rectG(m, 1, 1, 13, 8, t.wfloorB);
  // shelves: same item repeated 9 (C5)
  for (let c = 2; c <= 10; c++) setO(m, c, 1, t.shelfItem);   // 9 identical
  rectO(m, 1, 6, 8, 1, t.counter);
  setO(m, 12, 3, t.crate); setO(m, 13, 6, t.barrel); setO(m, 12, 7, t.crate);
  setG(m, 11, 0, t.windowI);
  exitDoor(m, 7, 9, 'plaza', 'sp_from_grocery');
  obj(m, 'sp_from_plaza', 'spawn', 7, 8);
  obj(m, 'mimi', 'npc', 4, 7, { npc: 'mimi' });
  obj(m, 'i_shelf', 'interact', 6, 2, { kind: 'shelf' });
  maps.grocery = m;
}

// ============ 5. CLINIC 12x8 ============
{
  const m = M(12, 8, t.wfloor); room(m);
  rectG(m, 1, 1, 10, 6, t.wfloorB);
  setO(m, 2, 2, t.table); setG(m, 2, 1, t.clinicForm);   // 表格 (C8)
  setO(m, 9, 1, t.bookshelf); setO(m, 9, 5, t.plantPot);
  setO(m, 5, 5, t.chair);
  setG(m, 6, 0, t.windowI);
  exitDoor(m, 6, 7, 'plaza', 'sp_from_clinic');
  obj(m, 'sp_from_plaza', 'spawn', 6, 6);
  obj(m, 'fu', 'npc', 4, 3, { npc: 'fu' });
  obj(m, 'i_clinicform', 'interact', 2, 2, { kind: 'clinicform' });
  maps.clinic = m;
}

// ============ 6. LAKE 24x16 ============
{
  const m = M(24, 16, t.grass);
  for (let r = 0; r < 16; r++) for (let c = 0; c < 24; c++) if (((c * 3 + r * 5) % 13) === 0) setG(m, c, r, t.grassF);
  // lake fills bottom rows 9-15
  for (let r = 9; r < 16; r++) for (let c = 1; c < 23; c++) {
    const deep = r >= 12;
    setG(m, c, r, deep ? t.waterDeep : (((c + r) % 2) ? t.water : t.waterL));
    setO(m, c, r, deep ? t.waterDeep : t.water);
  }
  // lilypads / reeds on shore
  setO(m, 3, 9, t.reed); setO(m, 20, 9, t.reed); setO(m, 12, 10, t.lilypad);
  borderO(m, t.fence);
  // top exit to plaza
  setO(m, 12, 0, 0); exitDoor(m, 12, 0, 'plaza', 'sp_from_lake'); setG(m, 12, 1, t.path); obj(m, 'sp_from_plaza', 'spawn', 12, 2);
  // bench (gui) at shore
  setO(m, 5, 8, t.bench);
  // picnic cloth (T7)
  setG(m, 16, 6, t.picnic);
  // trees
  setO(m, 2, 2, t.treeTrunk); setG(m, 2, 1, t.treeTop); setO(m, 21, 3, t.treeTrunk); setG(m, 21, 2, t.treeTop);
  setO(m, 8, 3, t.rock); setO(m, 18, 2, t.bushS);
  obj(m, 'gui', 'npc', 5, 7, { npc: 'gui' });
  obj(m, 'extra4', 'npc', 15, 3, { npc: 'extra4' });
  obj(m, 'i_picnic', 'interact', 16, 6, { kind: 'picnic' });
  maps.lake = m;
}

// ============ 7. FIELD 20x14 ============
{
  const m = M(20, 14, t.grass);
  for (let r = 0; r < 14; r++) for (let c = 0; c < 20; c++) {
    const k = (c * 7 + r * 3) % 9;
    if (k === 0) setG(m, c, r, t.grassF); else if (k === 3) setG(m, c, r, t.grassF2);
  }
  borderO(m, t.fence);
  // berry bushes (T6, pick x5) — 5 spread
  const berries = [[4, 3], [8, 4], [13, 3], [6, 8], [14, 9]];
  berries.forEach(([c, r], i) => { setO(m, c, r, t.berry); obj(m, 'i_berry' + i, 'interact', c, r, { kind: 'berry', n: i }); });
  // flowerbed (浇花点)
  setO(m, 10, 6, t.flowerbed); setO(m, 11, 6, t.flowerbed);
  obj(m, 'i_fieldbed', 'interact', 10, 7, { kind: 'flowerbed' });
  setO(m, 2, 2, t.treeTrunk); setG(m, 2, 1, t.treeTop); setO(m, 17, 11, t.stump); setO(m, 16, 4, t.mushroom);
  // top exit to plaza
  setO(m, 10, 0, 0); exitDoor(m, 10, 0, 'plaza', 'sp_from_field'); setG(m, 10, 1, t.path); obj(m, 'sp_from_plaza', 'spawn', 10, 2);
  obj(m, 'extra5', 'npc', 15, 7, { npc: 'extra5' });
  maps.field = m;
}

// ============ 8. WELL 14x10 ============
{
  const m = M(14, 10, t.grass);
  for (let r = 0; r < 10; r++) for (let c = 0; c < 14; c++) if (((c + r) % 5) === 0) setG(m, c, r, t.grassF);
  rectG(m, 5, 3, 4, 4, t.sand);
  borderO(m, t.fence);
  setO(m, 6, 4, t.well); setO(m, 7, 4, t.well);
  setO(m, 2, 2, t.rock); setO(m, 11, 7, t.bushS); setO(m, 3, 7, t.stump);
  // top exit to plaza
  setO(m, 7, 0, 0); exitDoor(m, 7, 0, 'plaza', 'sp_from_well'); setG(m, 7, 1, t.path); obj(m, 'sp_from_plaza', 'spawn', 7, 2);
  obj(m, 'i_well', 'interact', 7, 5, { kind: 'well' });
  maps.well = m;
}

// ============ 9. FOG 20x8 ============
{
  const m = M(20, 8, t.grass);
  for (let r = 0; r < 8; r++) for (let c = 0; c < 20; c++) if (((c * 2 + r) % 7) === 0) setG(m, c, r, t.grassF);
  // fog wall fills top rows 0-2 (collides)
  for (let r = 0; r < 3; r++) for (let c = 0; c < 20; c++) { setG(m, c, r, t.fog); setO(m, c, r, t.fog); }
  for (let r = 3; r < 8; r++) { setO(m, 0, r, t.fence); setO(m, 19, r, t.fence); }
  for (let c = 0; c < 20; c++) setO(m, c, 7, t.fence);
  setO(m, 3, 5, t.bushS); setO(m, 16, 5, t.bushS);
  // bottom exit to plaza
  setO(m, 10, 7, 0); exitDoor(m, 10, 7, 'plaza', 'sp_from_fog'); setG(m, 10, 6, t.path); obj(m, 'sp_from_plaza', 'spawn', 10, 5);
  obj(m, 'i_fog', 'interact', 10, 3, { kind: 'fog' });
  maps.fog = m;
}

// ============ 10. WHITE (corridor 20x6 stacked over ward) 20x14 ============
{
  const m = M(20, 14, t.wfloorW);
  rectG(m, 0, 0, 20, 14, t.wfloorW);
  rectG(m, 0, 0, 20, 6, t.wfloorWb);          // corridor top band
  borderO(m, t.wwall);
  // ward divider wall (row 6) with a doorway
  for (let c = 0; c < 20; c++) setO(m, c, 6, t.wwall);
  setO(m, 10, 6, 0); setG(m, 10, 6, t.wdoor);
  // corridor windows
  setG(m, 4, 0, t.wwindow); setG(m, 15, 0, t.wwindow);
  // ward: hospital beds
  setO(m, 3, 9, t.hbed); setO(m, 3, 11, t.hbed); setO(m, 16, 9, t.hbed);
  obj(m, 'sp_corridor', 'spawn', 10, 2);
  obj(m, 'sp_ward', 'spawn', 10, 11);
  maps.white = m;
}

// ================= TMX serialization =================
function tmx(m) {
  const csv = (arr) => {
    let out = '';
    for (let r = 0; r < m.h; r++) {
      out += arr.slice(r * m.w, r * m.w + m.w).join(',');
      if (r < m.h - 1) out += ',';
      out += '\n';
    }
    return out;
  };
  const objXml = m.objects.map((o) => {
    const props = Object.entries(o.props);
    const p = props.length
      ? `\n   <properties>\n${props.map(([k, v]) => `    <property name="${k}" value="${v}"/>`).join('\n')}\n   </properties>\n  `
      : '';
    return `  <object id="${o.id}" name="${o.name}" type="${o.type}" x="${o.x}" y="${o.y}" width="${o.w}" height="${o.h}">${p}</object>`;
  }).join('\n');
  const nextId = Math.max(1, ...m.objects.map((o) => o.id)) + 1;
  return `<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="${m.w}" height="${m.h}" tilewidth="16" tileheight="16" infinite="0" nextlayerid="5" nextobjectid="${nextId}">
 <tileset firstgid="1" name="tiles" tilewidth="16" tileheight="16" tilecount="72" columns="8">
  <image source="${PNG}" width="128" height="144"/>
 </tileset>
 <layer id="1" name="ground" width="${m.w}" height="${m.h}">
  <data encoding="csv">
${csv(m.ground)}</data>
 </layer>
 <layer id="2" name="obstacles" width="${m.w}" height="${m.h}">
  <data encoding="csv">
${csv(m.obs)}</data>
 </layer>
 <objectgroup id="3" name="objects">
${objXml}
 </objectgroup>
</map>
`;
}

for (const [name, m] of Object.entries(maps)) {
  writeFileSync(resolve(TMX_DIR, name + '.tmx'), tmx(m));
  console.log('wrote', name + '.tmx', m.w + 'x' + m.h, m.objects.length + ' objects');
}
console.log('MAPS TMX DONE');
