// Hand-authored top-down venue. Pure data + maths, no Phaser — the floor scene
// paints it, the AI walks it, the tests prove it hangs together.

export const TILE = 16;
export const MAP_W = 80; // tiles
export const MAP_H = 45; // tiles

export type TileKind =
  | 'void' | 'floor' | 'wall' | 'bar' | 'stool' | 'dance'
  | 'booth' | 'stall' | 'stallDoor' | 'exit' | 'smokeDoor' | 'neon'
  | 'djbooth' | 'djfloor' | 'yard' | 'fence' | 'sink';

export type AnchorKind = 'bar' | 'dance' | 'booth' | 'toilet' | 'smoke' | 'entry' | 'exit';

export interface TilePoint { tx: number; ty: number }
export interface WorldPoint { x: number; y: number }

export interface StallDef {
  id: string;              // 's1'..'s4'
  door: WorldPoint;        // world px, the tile just OUTSIDE the door (where you stand)
  inside: WorldPoint;      // world px, centre of the cubicle
}

export interface VenueMap {
  width: number;
  height: number;
  tiles: readonly TileKind[];            // row-major, length MAP_W*MAP_H
  anchors: Readonly<Record<AnchorKind, readonly WorldPoint[]>>;  // world px
  stalls: readonly StallDef[];
}

// 'stall' is walkable, but each cubicle interior is sealed behind a non-walkable
// 'stallDoor' — no path leads in. Movers must not pick stall tiles as pathing
// targets; send a patron inside via StallDef.inside instead. Everything else
// walkable is mutually reachable (see tests/floor/venueMap.test.ts).
const WALKABLE: ReadonlySet<TileKind> = new Set<TileKind>([
  'floor', 'dance', 'exit', 'smokeDoor', 'stall', 'yard',
]);

// Tuned UNDER the renderer's darkness sheet, not in isolation — at the original
// values the venue multiplied down to near-black and you couldn't tell the dance
// floor from a wall. Dark enough that you need the torch, light enough to navigate.
const COLOURS: Record<TileKind, number> = {
  void: 0x05050a,
  floor: 0x221e2b,
  wall: 0x3a3448,
  bar: 0x5c3f28,
  stool: 0x6d4726,
  dance: 0x33284a,
  booth: 0x452c3c,
  stall: 0x2e3838,
  stallDoor: 0x4c6058,
  exit: 0x2c5636,
  smokeDoor: 0x3d4d5c,
  neon: 0xd8409a,
  djbooth: 0x2c3550,
  djfloor: 0x232a42,
  yard: 0x28303a,
  fence: 0x46505a,
  sink: 0x4a5a5c,
};

// Cubicle west edges. Each cubicle is a 4x4 walled box; the last one shares the
// dunny room's east wall, which is why they stop at 73. CUBICLE_Y sits flush
// under the room's north wall — a gap there would strand a dead strip of floor.
const CUBICLE_X = [61, 65, 69, 73] as const;
const CUBICLE_Y = 4;

function buildTiles(): TileKind[] {
  const tiles = new Array<TileKind>(MAP_W * MAP_H).fill('floor');
  const set = (tx: number, ty: number, kind: TileKind): void => {
    if (tx < 0 || tx >= MAP_W || ty < 0 || ty >= MAP_H) return;
    tiles[ty * MAP_W + tx] = kind;
  };
  const rect = (x0: number, y0: number, x1: number, y1: number, kind: TileKind): void => {
    for (let ty = y0; ty <= y1; ty++) for (let tx = x0; tx <= x1; tx++) set(tx, ty, kind);
  };
  const box = (x0: number, y0: number, x1: number, y1: number, kind: TileKind): void => {
    rect(x0, y0, x1, y0, kind);
    rect(x0, y1, x1, y1, kind);
    rect(x0, y0, x0, y1, kind);
    rect(x1, y0, x1, y1, kind);
  };

  box(0, 0, MAP_W - 1, MAP_H - 1, 'wall');

  // Dead space west of the main room, carved back out for the entry corridor.
  rect(1, 1, 13, MAP_H - 2, 'wall');
  rect(1, 21, 14, 23, 'floor');
  rect(1, 21, 1, 23, 'exit');

  // Bar run along the north wall, stools hugging its south face.
  rect(22, 4, 52, 5, 'bar');
  rect(22, 6, 52, 6, 'stool');

  rect(26, 16, 50, 30, 'dance');

  // Booths: south wall (east of the smoking yard) then east wall.
  for (const tx of [25, 32, 39, 46]) rect(tx, 41, tx + 1, 42, 'booth');
  for (const ty of [22, 27, 32]) rect(76, ty, 77, ty + 1, 'booth');

  // DJ booth: sealed island east of the dance floor, gear facing it across the
  // west counter. Nobody paths inside — like the cubicles, the interior is a
  // stage, not a room (the DJ is set dressing until the DJ role lands).
  rect(52, 19, 58, 25, 'djbooth');
  rect(53, 20, 57, 24, 'djfloor');

  // Smoking yard: fenced open-sky pocket in the south-west. One door in from
  // the floor; rain falls INSIDE the fence (the renderer keys off 'yard').
  rect(14, 38, 22, 43, 'fence');
  rect(15, 39, 21, 42, 'yard');
  set(18, 38, 'smokeDoor');

  // Dunnies get plumbing: sink row along the south interior wall.
  rect(66, 15, 71, 15, 'sink');

  // Dunnies: walled room in the north-east, one doorway on its south face.
  box(60, 3, 76, 16, 'wall');
  rect(62, 16, 63, 16, 'floor');
  for (const x of CUBICLE_X) {
    box(x, CUBICLE_Y, x + 3, CUBICLE_Y + 3, 'wall');
    rect(x + 1, CUBICLE_Y + 1, x + 2, CUBICLE_Y + 2, 'stall');
    set(x + 1, CUBICLE_Y + 3, 'stallDoor');
  }

  // Accent strips the renderer hangs point lights off.
  for (const tx of [20, 30, 40, 50, 60]) set(tx, 0, 'neon');
  for (const ty of [10, 20, 30]) set(MAP_W - 1, ty, 'neon');

  return tiles;
}

export function worldToTile(x: number, y: number): TilePoint {
  return { tx: Math.floor(x / TILE), ty: Math.floor(y / TILE) };
}

/** Centre of the tile, not its corner — patrons stand on centres. */
export function tileToWorld(tx: number, ty: number): WorldPoint {
  return { x: tx * TILE + TILE / 2, y: ty * TILE + TILE / 2 };
}

function buildStalls(): StallDef[] {
  return CUBICLE_X.map((x, i) => ({
    id: `s${i + 1}`,
    door: tileToWorld(x + 1, CUBICLE_Y + 4),
    // Centre of the 2x2 interior, which lands on a tile corner rather than a centre.
    inside: { x: (x + 2) * TILE, y: (CUBICLE_Y + 2) * TILE },
  }));
}

function buildAnchors(): Record<AnchorKind, readonly WorldPoint[]> {
  const dance: WorldPoint[] = [];
  for (const ty of [20, 26]) for (const tx of [30, 36, 42, 48]) dance.push(tileToWorld(tx, ty));
  return {
    bar: [24, 29, 34, 39, 44, 49].map((tx) => tileToWorld(tx, 7)),
    dance,
    booth: [
      ...[25, 32, 39, 46].map((tx) => tileToWorld(tx, 40)),
      tileToWorld(75, 22),
      tileToWorld(75, 27),
    ],
    toilet: CUBICLE_X.map((x) => tileToWorld(x + 1, CUBICLE_Y + 4)),
    smoke: [tileToWorld(17, 41), tileToWorld(19, 40)],
    entry: [tileToWorld(4, 22), tileToWorld(9, 22)],
    exit: [tileToWorld(1, 22)],
  };
}

// ---- props & lights (docs/SPACES.md) -----------------------------------------
//
// Props are DATA: the renderer prefers a generated texture ('gen:prop:<kind>',
// dropped in public/props/ + manifest — see tools/props_import.py) and falls
// back to a baked placeholder. Footprints are tiles; props never affect
// walkability — anything that should block stands on non-walkable tiles.

export type PropKind =
  | 'deck' | 'mixer' | 'monitor' | 'boothLamp'
  | 'speaker' | 'discoball'
  | 'barShelf' | 'taps' | 'till' | 'glassStack' | 'glassRack'
  | 'boothTable' | 'highTable' | 'sinkRow'
  | 'buttBin' | 'crate' | 'plant' | 'wetFloor' | 'poolTable'
  | 'poster1' | 'poster2' | 'poster3' | 'cloak' | 'stampPodium' | 'arcade';

export interface PropEmissive {
  colour: number;
  radius: number;
  pulse?: 'beat' | 'flicker';
}

export interface PropDef {
  id: string;
  kind: PropKind;
  tx: number;
  ty: number;
  tw: number;
  th: number;
  emissive?: PropEmissive;
}

export interface ZoneLight {
  id: string;
  tx: number;
  ty: number;
  colour: number;
  radius: number;
  pulse?: 'beat' | 'flicker';
}

const P = (
  id: string, kind: PropKind, tx: number, ty: number, tw: number, th: number,
  emissive?: PropEmissive,
): PropDef => (emissive ? { id, kind, tx, ty, tw, th, emissive } : { id, kind, tx, ty, tw, th });

export const PROPS: readonly PropDef[] = Object.freeze([
  // DJ booth — the fleet's own gear, facing the dance floor
  P('deckA', 'deck', 53, 20, 2, 2),
  P('mixer', 'mixer', 53, 22, 2, 1),
  P('deckB', 'deck', 53, 23, 2, 2),
  P('monitor', 'monitor', 56, 22, 1, 1),
  P('boothLamp', 'boothLamp', 57, 20, 1, 1, { colour: 0x3060c0, radius: 40, pulse: 'flicker' }),
  // dance floor
  P('spkNW', 'speaker', 25, 14, 1, 2),
  P('spkNE', 'speaker', 50, 14, 1, 2),
  P('spkSW', 'speaker', 25, 31, 1, 2),
  P('spkSE', 'speaker', 50, 31, 1, 2),
  P('ball', 'discoball', 37, 22, 2, 2, { colour: 0xe8c8e0, radius: 22 }),
  // bar
  P('shelf', 'barShelf', 22, 3, 31, 1),
  P('glassStack', 'glassStack', 24, 4, 2, 1),
  P('taps', 'taps', 32, 4, 3, 1),
  P('till', 'till', 46, 4, 2, 1),
  // glassie station (future role home — design §6)
  P('rackA', 'glassRack', 16, 4, 2, 1),
  P('rackB', 'glassRack', 16, 6, 2, 1),
  // booths
  P('tblS1', 'boothTable', 25, 40, 2, 1, { colour: 0xd8a050, radius: 12 }),
  P('tblS2', 'boothTable', 32, 40, 2, 1, { colour: 0xd8a050, radius: 12 }),
  P('tblS3', 'boothTable', 39, 40, 2, 1, { colour: 0xd8a050, radius: 12 }),
  P('tblS4', 'boothTable', 46, 40, 2, 1, { colour: 0xd8a050, radius: 12 }),
  P('tblE1', 'boothTable', 75, 23, 1, 1, { colour: 0xd8a050, radius: 12 }),
  P('tblE2', 'boothTable', 75, 28, 1, 1, { colour: 0xd8a050, radius: 12 }),
  // dunnies
  P('sinks', 'sinkRow', 66, 15, 6, 1),
  // smoking yard
  P('bin', 'buttBin', 16, 39, 1, 1),
  P('crateA', 'crate', 19, 42, 1, 1),
  P('crateB', 'crate', 20, 40, 1, 1),
  // entry corridor
  P('poster1', 'poster1', 3, 20, 2, 1),
  P('poster2', 'poster2', 7, 20, 2, 1),
  P('poster3', 'poster3', 11, 20, 2, 1),
  P('cloak', 'cloak', 14, 18, 1, 3),
  P('podium', 'stampPodium', 12, 22, 1, 1),
  // one arcade cabinet, because it is a venue in Australia
  P('arcade', 'arcade', 15, 34, 2, 2, { colour: 0x40c0d0, radius: 18, pulse: 'flicker' }),
  // filling out the room (2026-07-20): the club stops being furniture-poor
  P('tblE3', 'boothTable', 75, 33, 1, 1, { colour: 0xd8a050, radius: 12 }),
  P('high1', 'highTable', 28, 34, 1, 1),
  P('high2', 'highTable', 46, 34, 1, 1),
  P('plant1', 'plant', 14, 25, 1, 1),
  P('plant2', 'plant', 21, 36, 1, 1),
  P('wet', 'wetFloor', 35, 8, 1, 1),
  // the pool corner — it is still an Australian venue underneath the techno
  P('pool', 'poolTable', 15, 10, 3, 2),
]);

const L = (
  id: string, tx: number, ty: number, colour: number, radius: number,
  pulse?: 'beat' | 'flicker',
): ZoneLight => (pulse ? { id, tx, ty, colour, radius, pulse } : { id, tx, ty, colour, radius });

// Zone light signatures (docs/SPACES.md §1): navigate by glow colour.
export const LIGHTS: readonly ZoneLight[] = Object.freeze([
  // bar ambers along the shelf
  ...[24, 30, 36, 42, 48].map((tx, i) => L(`bar${i}`, tx, 4, 0xd8a020, 40)),
  // dance grid — pulses on the ACTUAL beat via FloorView.onBeat
  ...[30, 37, 44].flatMap((tx) =>
    [19, 23, 27].map((ty) => L(`dg${tx}x${ty}`, tx, ty, 0xd03470, 26, 'beat')),
  ),
  L('dj', 55, 22, 0x3060c0, 46),
  L('loo', 68, 10, 0x50c060, 55, 'flicker'),
  L('yard', 18, 40, 0x6080a0, 44),
  L('entry', 2, 22, 0xd03470, 40),
  L('glassie', 17, 5, 0xd8a020, 28),
  // low green cone over the pool table, like every pool table ever
  L('pool', 16, 10, 0x30a050, 34),
]);

export const VENUE: VenueMap = Object.freeze({
  width: MAP_W,
  height: MAP_H,
  tiles: Object.freeze(buildTiles()),
  anchors: Object.freeze(buildAnchors()),
  stalls: Object.freeze(buildStalls()),
});

export function tileAt(map: VenueMap, tx: number, ty: number): TileKind {
  if (tx < 0 || tx >= map.width || ty < 0 || ty >= map.height) return 'void';
  return map.tiles[ty * map.width + tx] ?? 'void';
}

export function isWalkable(map: VenueMap, tx: number, ty: number): boolean {
  return WALKABLE.has(tileAt(map, tx, ty));
}

export function isWalkableWorld(map: VenueMap, x: number, y: number): boolean {
  const { tx, ty } = worldToTile(x, y);
  return isWalkable(map, tx, ty);
}

/** Colour for painting a tile in the dark venue. */
export function tileColour(kind: TileKind): number {
  return COLOURS[kind];
}
