/* The city: layout, districts, and the collision grid.
 *
 * The five district names come from the arena prototype in
 * nexus-studio-unified.html, which decided them with bare coordinate bands and
 * nothing else - getZone() returned a string and no code ever read it. Here a
 * district is real data: it owns a palette, a building height range, how dense
 * the traffic and crowds are, and how hard the police push back.
 *
 * COLLISION lives here too, and it is a tile grid on purpose. Buildings are
 * generated as rectangles, but testing a circle against every rectangle in a
 * 160x160-tile city is 400+ tests per entity per frame. Stamping those
 * rectangles into a Uint8Array once at generation time turns the same query
 * into "read the 9 tiles around this point", which is what makes a few hundred
 * pedestrians, cars and bullets affordable.
 */
const TILE = 40;
const MAP_W = 168;                 // tiles
const MAP_H = 168;
const WORLD_W = MAP_W * TILE;      // 6720 px
const WORLD_H = MAP_H * TILE;

const T_ROAD = 0, T_WALK = 1, T_BUILD = 2, T_PARK = 3, T_LOT = 4, T_WATER = 5;

/* Street grid. A 12-tile cell = 3 tiles of road, 1 of pavement each side, and
 * a 7x7 interior for buildings. At TILE=40 that is a 120px road - wide enough
 * for two cars to pass, which is the number that actually matters, since a car
 * sprite is ~70px long and ~30px wide. */
const CELL = 12, ROAD_W = 3;

/* Districts differ by palette AND by silhouette - Downtown builds tall and
 * narrow, Southway builds low and wide - because colour alone is not enough to
 * know where you are at a glance while driving. */
const DISTRICTS = {
  Downtown:  { name: 'Downtown',  wall: '#2a3145', roof: '#59637e', trim: '#7c88a8',
               glass: '#8fa6d8',
               hMin: 34, hMax: 76, peds: 1.3, traffic: 1.2, heat: 1.0, big: 0.30,
               blurb: 'Glass and money. The police here are quick.' },
  Northgate: { name: 'Northgate', wall: '#3a2b22', roof: '#7a5340', trim: '#9c6a4e',
               glass: '#d8a878',
               hMin: 18, hMax: 38, peds: 0.9, traffic: 1.0, heat: 0.8, big: 0.55,
               blurb: 'Old brick, narrow lots, long memories.' },
  Eastpark:  { name: 'Eastpark',  wall: '#26382c', roof: '#4f7a58', trim: '#6d9a76',
               glass: '#bcd9a8',
               hMin: 12, hMax: 26, peds: 1.1, traffic: 0.7, heat: 0.7, big: 0.70,
               blurb: 'Green and quiet. Nothing happens here until it does.' },
  Southway:  { name: 'Southway',  wall: '#33242c', roof: '#6b4a52', trim: '#8d626c',
               glass: '#c98f92',
               hMin: 14, hMax: 28, peds: 0.7, traffic: 1.4, heat: 1.2, big: 0.82,
               blurb: 'Docks and warehouses. Cheap rent, expensive habits.' },
  Westside:  { name: 'Westside',  wall: '#2c2f36', roof: '#5a5f6d', trim: '#7b8191',
               glass: '#a8b0c4',
               hMin: 22, hMax: 46, peds: 1.0, traffic: 1.1, heat: 0.9, big: 0.48,
               blurb: 'Where the city keeps the things it still uses.' },
};

/* Deterministic PRNG. The city has to come out the same on every load or the
 * saved position from localStorage could put the player inside a wall. */
function mulberry32(a) {
  return function () {
    a |= 0; a = (a + 0x6D2B79F5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

const World = {
  tiles: null,
  buildings: [],
  props: [],
  seed: 20260811,

  districtAt(x, y) {
    // Same precedence as the prototype's getZone, so the map reads the way it
    // did: corners resolve to the left/right district before top/bottom.
    if (x < WORLD_W * 0.26) return DISTRICTS.Westside;
    if (x > WORLD_W * 0.74) return DISTRICTS.Eastpark;
    if (y < WORLD_H * 0.26) return DISTRICTS.Northgate;
    if (y > WORLD_H * 0.74) return DISTRICTS.Southway;
    return DISTRICTS.Downtown;
  },

  generate(seed) {
    this.seed = seed || this.seed;
    const rnd = mulberry32(this.seed);
    const t = new Uint8Array(MAP_W * MAP_H);
    this.buildings = [];
    this.props = [];

    // 1. the street grid
    for (let y = 0; y < MAP_H; y++) {
      for (let x = 0; x < MAP_W; x++) {
        const rx = x % CELL, ry = y % CELL;
        const roadX = rx < ROAD_W, roadY = ry < ROAD_W;
        const walkX = rx === ROAD_W || rx === CELL - 1;
        const walkY = ry === ROAD_W || ry === CELL - 1;
        let v;
        if (roadX || roadY) v = T_ROAD;
        else if (walkX || walkY) v = T_WALK;
        else v = T_BUILD;          // provisional; blocks are carved next
        t[y * MAP_W + x] = v;
      }
    }

    // 2. fill each block interior
    for (let by = 0; by < MAP_H; by += CELL) {
      for (let bx = 0; bx < MAP_W; bx += CELL) {
        const x0 = bx + ROAD_W + 1, y0 = by + ROAD_W + 1;
        const x1 = bx + CELL - 2, y1 = by + CELL - 2;   // inclusive
        if (x1 >= MAP_W || y1 >= MAP_H) {
          for (let y = y0; y <= Math.min(y1, MAP_H - 1); y++)
            for (let x = x0; x <= Math.min(x1, MAP_W - 1); x++)
              t[y * MAP_W + x] = T_WALK;
          continue;
        }
        const d = this.districtAt((bx + CELL / 2) * TILE, (by + CELL / 2) * TILE);
        const roll = rnd();

        if (roll < 0.13) {                       // park
          for (let y = y0; y <= y1; y++) for (let x = x0; x <= x1; x++) t[y * MAP_W + x] = T_PARK;
          const n = 2 + (rnd() * 4 | 0);
          for (let i = 0; i < n; i++) {
            this.props.push({ kind: 'tree',
              x: (x0 + rnd() * (x1 - x0 + 1)) * TILE, y: (y0 + rnd() * (y1 - y0 + 1)) * TILE,
              r: 9 + rnd() * 7 });
          }
        } else if (roll < 0.22) {                // car park
          for (let y = y0; y <= y1; y++) for (let x = x0; x <= x1; x++) t[y * MAP_W + x] = T_LOT;
        } else {
          // 1-4 buildings with alleys between them
          this._carveBlock(t, x0, y0, x1, y1, d, rnd);
        }
      }
    }

    // 3. a hard border so nothing can drive off the edge into nothing
    for (let x = 0; x < MAP_W; x++) {
      t[x] = T_WATER; t[MAP_W + x] = T_WATER;
      t[(MAP_H - 1) * MAP_W + x] = T_WATER; t[(MAP_H - 2) * MAP_W + x] = T_WATER;
    }
    for (let y = 0; y < MAP_H; y++) {
      t[y * MAP_W] = T_WATER; t[y * MAP_W + 1] = T_WATER;
      t[y * MAP_W + MAP_W - 1] = T_WATER; t[y * MAP_W + MAP_W - 2] = T_WATER;
    }

    this.tiles = t;
    return this;
  },

  _carveBlock(t, x0, y0, x1, y1, d, rnd) {
    // The grid pass marked this whole interior T_BUILD provisionally. Clear it
    // back to walkable first: buildings are stamped in below, and whatever is
    // left over is alley. Skipping this makes every alley solid, which looks
    // completely normal from above and silently walls the player in.
    for (let y = y0; y <= y1; y++)
      for (let x = x0; x <= x1; x++) t[y * MAP_W + x] = T_WALK;

    const bw = x1 - x0 + 1, bh = y1 - y0 + 1;
    // How often a block splits is a district trait: Downtown packs four narrow
    // towers into a plot, Southway puts one wide warehouse on it. Silhouette
    // does more than colour to tell you where you are while driving past.
    const split = 1 - (d.big !== undefined ? d.big : 0.5);
    const cols = bw >= 6 && rnd() < split ? 2 : 1;
    const rows = bh >= 6 && rnd() < split ? 2 : 1;
    const cw = (bw - (cols - 1)) / cols;         // one tile of alley between
    const chh = (bh - (rows - 1)) / rows;

    for (let r = 0; r < rows; r++) {
      for (let c = 0; c < cols; c++) {
        const sx = Math.round(x0 + c * (cw + 1));
        const sy = Math.round(y0 + r * (chh + 1));
        const ex = Math.min(x1, Math.round(sx + cw - 1));
        const ey = Math.min(y1, Math.round(sy + chh - 1));
        if (ex < sx || ey < sy) continue;
        if (rnd() < 0.08) continue;              // occasional empty plot

        for (let y = sy; y <= ey; y++)
          for (let x = sx; x <= ex; x++) t[y * MAP_W + x] = T_BUILD;

        const h = d.hMin + rnd() * (d.hMax - d.hMin);
        this.buildings.push({
          x: sx * TILE, y: sy * TILE,
          w: (ex - sx + 1) * TILE, h: (ey - sy + 1) * TILE,
          height: h, d: d,
          // a stable per-building tint so the skyline is not flat
          shade: 0.82 + rnd() * 0.32,
          lit: rnd() < 0.4,
        });
      }
    }
  },

  tileAt(tx, ty) {
    if (tx < 0 || ty < 0 || tx >= MAP_W || ty >= MAP_H) return T_WATER;
    return this.tiles[ty * MAP_W + tx];
  },

  solidTile(tx, ty) {
    const v = this.tileAt(tx, ty);
    return v === T_BUILD || v === T_WATER;
  },

  solidAt(x, y) {
    return this.solidTile((x / TILE) | 0, (y / TILE) | 0);
  },

  isRoad(x, y) {
    return this.tileAt((x / TILE) | 0, (y / TILE) | 0) === T_ROAD;
  },

  /* Push a circle out of any solid tile it overlaps.
   *
   * Resolving on the single smallest axis per tile, rather than summing every
   * overlap, is what stops the classic corner bug where two adjacent walls
   * each push the body out and their sum launches it diagonally through the
   * corner. It also means sliding along a wall Just Works: the tangential
   * component is never touched.
   *
   * Returns {x, y, hit, nx, ny} - the normal is what vehicles use to decide
   * how hard they just crashed. */
  resolveCircle(x, y, r) {
    let hit = false, nx = 0, ny = 0;
    const t0x = ((x - r) / TILE) | 0, t1x = ((x + r) / TILE) | 0;
    const t0y = ((y - r) / TILE) | 0, t1y = ((y + r) / TILE) | 0;

    for (let ty = t0y; ty <= t1y; ty++) {
      for (let tx = t0x; tx <= t1x; tx++) {
        if (!this.solidTile(tx, ty)) continue;
        const bx = tx * TILE, by = ty * TILE;
        // closest point on the tile to the circle centre
        const cx = x < bx ? bx : (x > bx + TILE ? bx + TILE : x);
        const cy = y < by ? by : (y > by + TILE ? by + TILE : y);
        let dx = x - cx, dy = y - cy;
        let d2 = dx * dx + dy * dy;

        if (d2 > r * r) continue;

        if (d2 > 1e-6) {                       // centre outside the tile: push along the normal
          const d = Math.sqrt(d2);
          const push = r - d;
          dx /= d; dy /= d;
          x += dx * push; y += dy * push;
          nx += dx; ny += dy;
        } else {                               // centre inside the tile: eject on the shallowest face
          const l = x - bx, rr = bx + TILE - x, u = y - by, dn = by + TILE - y;
          const m = Math.min(l, rr, u, dn);
          if (m === l)       { x = bx - r;         nx -= 1; }
          else if (m === rr) { x = bx + TILE + r;  nx += 1; }
          else if (m === u)  { y = by - r;         ny -= 1; }
          else               { y = by + TILE + r;  ny += 1; }
        }
        hit = true;
      }
    }
    const nl = Math.hypot(nx, ny) || 1;
    return { x: x, y: y, hit: hit, nx: nx / nl, ny: ny / nl };
  },

  /* Straight-line visibility, used by the police to decide whether they can
   * see the player and by pedestrians to decide whether to be afraid.
   * Stepping a whole tile at a time is coarse but this runs per-officer
   * per-frame and a false positive at a corner costs nothing. */
  lineClear(x0, y0, x1, y1) {
    const dx = x1 - x0, dy = y1 - y0;
    const dist = Math.hypot(dx, dy);
    const steps = Math.ceil(dist / (TILE * 0.6));
    if (steps <= 1) return true;
    for (let i = 1; i < steps; i++) {
      const t = i / steps;
      if (this.solidAt(x0 + dx * t, y0 + dy * t)) return false;
    }
    return true;
  },

  /* A free spot on a road, for spawning traffic and police. */
  randomRoad(rnd, nearX, nearY, minR, maxR) {
    for (let i = 0; i < 220; i++) {
      let x, y;
      if (nearX === undefined) {
        x = rnd() * WORLD_W; y = rnd() * WORLD_H;
      } else {
        const a = rnd() * Math.PI * 2;
        const d = minR + rnd() * (maxR - minR);
        x = nearX + Math.cos(a) * d; y = nearY + Math.sin(a) * d;
        if (x < TILE * 3 || y < TILE * 3 || x > WORLD_W - TILE * 3 || y > WORLD_H - TILE * 3) continue;
      }
      if (this.isRoad(x, y)) return { x: x, y: y };
    }
    return null;
  },

  randomWalk(rnd, nearX, nearY, minR, maxR) {
    for (let i = 0; i < 220; i++) {
      const a = rnd() * Math.PI * 2;
      const d = minR + rnd() * (maxR - minR);
      const x = nearX + Math.cos(a) * d, y = nearY + Math.sin(a) * d;
      if (x < TILE * 3 || y < TILE * 3 || x > WORLD_W - TILE * 3 || y > WORLD_H - TILE * 3) continue;
      const v = this.tileAt((x / TILE) | 0, (y / TILE) | 0);
      if (v === T_WALK || v === T_PARK || v === T_LOT) return { x: x, y: y };
    }
    return null;
  },

  /* ── rendering ──────────────────────────────────────────────────────────
   * Ground first, in tiles, clipped to the camera. Buildings are drawn
   * separately and last (see drawBuildings) because their roofs have to sit on
   * top of cars and people. */
  drawGround(ctx, cam) {
    const t0x = Math.max(0, (cam.x0 / TILE) | 0), t1x = Math.min(MAP_W - 1, (cam.x1 / TILE) | 0);
    const t0y = Math.max(0, (cam.y0 / TILE) | 0), t1y = Math.min(MAP_H - 1, (cam.y1 / TILE) | 0);

    for (let ty = t0y; ty <= t1y; ty++) {
      for (let tx = t0x; tx <= t1x; tx++) {
        const v = this.tiles[ty * MAP_W + tx];
        if (v === T_BUILD) continue;           // drawn later, as a solid
        const x = tx * TILE, y = ty * TILE;
        let c;
        if (v === T_ROAD) c = '#24262c';
        else if (v === T_WALK) c = '#3a3d45';
        else if (v === T_PARK) c = '#2b4030';
        else if (v === T_LOT) c = '#2e3036';
        else c = '#141c28';                     // water / out of bounds
        ctx.fillStyle = c;
        ctx.fillRect(x, y, TILE + 1, TILE + 1);
      }
    }

    // lane markings: a dash down the centre of each road strip
    ctx.strokeStyle = 'rgba(210,190,90,.30)';
    ctx.lineWidth = 2;
    ctx.setLineDash([16, 22]);
    ctx.beginPath();
    const c0x = Math.floor(cam.x0 / (CELL * TILE)) * CELL;
    const c1x = Math.ceil(cam.x1 / (CELL * TILE)) * CELL;
    for (let cx = c0x; cx <= c1x; cx += CELL) {
      const px = (cx + ROAD_W / 2) * TILE;
      ctx.moveTo(px, cam.y0); ctx.lineTo(px, cam.y1);
    }
    const c0y = Math.floor(cam.y0 / (CELL * TILE)) * CELL;
    const c1y = Math.ceil(cam.y1 / (CELL * TILE)) * CELL;
    for (let cy = c0y; cy <= c1y; cy += CELL) {
      const py = (cy + ROAD_W / 2) * TILE;
      ctx.moveTo(cam.x0, py); ctx.lineTo(cam.x1, py);
    }
    ctx.stroke();
    ctx.setLineDash([]);

    /* Zebra crossings where the pavement meets the road. These are drawn from
     * the same CELL arithmetic the grid was built from rather than from a
     * stored list, so they land exactly on the kerb line at every junction and
     * cost one loop over the visible cells. */
    ctx.fillStyle = 'rgba(226,232,240,.13)';
    for (let cy = c0y; cy <= c1y; cy += CELL) {
      for (let cx = c0x; cx <= c1x; cx += CELL) {
        for (let i = 0; i < 5; i++) {
          const px = (cx + ROAD_W) * TILE + 4;
          const py = (cy + ROAD_W) * TILE + 4;
          // one strip below the junction, one to its right
          ctx.fillRect((cx + 0.15) * TILE + i * 22, py - 12, 12, 9);
          ctx.fillRect(px - 12, (cy + 0.15) * TILE + i * 22, 9, 12);
        }
      }
    }

    // park trees
    for (const p of this.props) {
      if (p.x < cam.x0 - 40 || p.x > cam.x1 + 40 || p.y < cam.y0 - 40 || p.y > cam.y1 + 40) continue;
      ctx.fillStyle = 'rgba(0,0,0,.35)';
      ctx.beginPath(); ctx.arc(p.x + 3, p.y + 4, p.r, 0, 6.284); ctx.fill();
      ctx.fillStyle = '#2f5c38';
      ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, 6.284); ctx.fill();
      ctx.fillStyle = '#3c7345';
      ctx.beginPath(); ctx.arc(p.x - p.r * .2, p.y - p.r * .2, p.r * .6, 0, 6.284); ctx.fill();
    }
  },

  /* Buildings, GTA-style: the footprint is where the wall meets the ground and
   * is exactly the collision box; the roof is the same rectangle pushed away
   * from the screen centre in proportion to its height. That single offset is
   * what sells the third dimension - the further a building is from where you
   * are standing, the more of its side you see, which is how real parallax
   * behaves.
   *
   * Drawn AFTER entities so a player who walks behind a tower is correctly
   * hidden by it. */
  drawBuildings(ctx, cam) {
    const ccx = (cam.x0 + cam.x1) / 2, ccy = (cam.y0 + cam.y1) / 2;
    const K = 0.0038;                          // parallax per unit of height
    const CAP = 72;                            // and never more than this

    for (const b of this.buildings) {
      if (b.x > cam.x1 + 200 || b.x + b.w < cam.x0 - 200 ||
          b.y > cam.y1 + 200 || b.y + b.h < cam.y0 - 200) continue;

      const cx = b.x + b.w / 2, cy = b.y + b.h / 2;
      /* Offset grows with height AND distance from the centre of the view,
       * which is what real parallax does - but left uncapped it reaches ~190px
       * at the screen edge for a tall tower, and a roof displaced that far
       * covers the entire street beside it. The cap keeps the near-centre
       * depth honest, where the player actually is, and stops the far edge
       * from turning into overlapping slabs. */
      let ox = (cx - ccx) * b.height * K;
      let oy = (cy - ccy) * b.height * K;
      const m = Math.hypot(ox, oy);
      if (m > CAP) { ox = ox / m * CAP; oy = oy / m * CAP; }
      const rx = b.x + ox, ry = b.y + oy;

      // 1. ground shadow, cast from a fixed sun so the whole city agrees on
      //    where the light is. Without it the towers look pasted on.
      const sl = b.height * 0.16;
      ctx.fillStyle = 'rgba(0,0,0,.34)';
      ctx.fillRect(b.x + sl * 0.5, b.y + sl, b.w, b.h);

      // 2. the four walls. Only the two facing the camera are actually
      //    visible, but drawing all four costs nothing and means the
      //    silhouette is right at every position. Each face gets its own
      //    brightness, which is the thing that makes the shape read as solid:
      //    a single flat wall colour looks like a smudge.
      const wallN = b.wallN || (b.wallN = shade(b.d.wall, 1.30));
      const wallS = b.wallS || (b.wallS = shade(b.d.wall, 0.72));
      const wallE = b.wallE || (b.wallE = shade(b.d.wall, 1.06));
      const wallW = b.wallW || (b.wallW = shade(b.d.wall, 0.88));
      const face = (ax, ay, bx2, by2, col) => {
        ctx.fillStyle = col;
        ctx.beginPath();
        ctx.moveTo(ax, ay); ctx.lineTo(bx2, by2);
        ctx.lineTo(bx2 + ox, by2 + oy); ctx.lineTo(ax + ox, ay + oy);
        ctx.closePath(); ctx.fill();
      };
      face(b.x, b.y, b.x + b.w, b.y, oy < 0 ? wallN : wallS);
      face(b.x, b.y + b.h, b.x + b.w, b.y + b.h, oy < 0 ? wallS : wallN);
      face(b.x, b.y, b.x, b.y + b.h, ox < 0 ? wallE : wallW);
      face(b.x + b.w, b.y, b.x + b.w, b.y + b.h, ox < 0 ? wallW : wallE);

      // 3. windows down whichever wall faces us, so height is legible
      if (b.height > 20) {
        ctx.fillStyle = b.d.glass;
        ctx.globalAlpha = 0.20;
        const rows = Math.max(1, Math.round(b.height / 13));
        if (Math.abs(oy) > 5) {
          const y0 = oy < 0 ? b.y : b.y + b.h;
          for (let c = 0; c < Math.max(1, (b.w / 26) | 0); c++) {
            for (let r = 0; r < rows; r++) {
              const t = (r + 0.30) / rows;
              ctx.fillRect(b.x + 9 + c * 26 + ox * t, y0 + oy * t - 3, 12, 5);
            }
          }
        }
        if (Math.abs(ox) > 5) {
          const x0 = ox < 0 ? b.x : b.x + b.w;
          for (let c = 0; c < Math.max(1, (b.h / 26) | 0); c++) {
            for (let r = 0; r < rows; r++) {
              const t = (r + 0.30) / rows;
              ctx.fillRect(x0 + ox * t - 3, b.y + 9 + c * 26 + oy * t, 5, 12);
            }
          }
        }
        ctx.globalAlpha = 1;
      }

      // 4. roof
      ctx.fillStyle = b.roofCache || (b.roofCache = shade(b.d.roof, b.shade));
      ctx.fillRect(rx, ry, b.w, b.h);
      ctx.strokeStyle = b.d.trim;
      ctx.lineWidth = 2;
      ctx.strokeRect(rx + 1, ry + 1, b.w - 2, b.h - 2);

      // roof clutter: a vent block and, on some, a lit stairwell head
      ctx.fillStyle = 'rgba(0,0,0,.22)';
      ctx.fillRect(rx + b.w * 0.16, ry + b.h * 0.62, b.w * 0.22, b.h * 0.2);
      if (b.lit && b.w > 80 && b.h > 80) {
        ctx.fillStyle = 'rgba(255,214,120,.16)';
        ctx.fillRect(rx + b.w * .55, ry + b.h * .2, b.w * .28, b.h * .26);
      }
    }
    ctx.globalAlpha = 1;
  },
};

function shade(hex, k) {
  const n = parseInt(hex.slice(1), 16);
  const r = Math.min(255, ((n >> 16) & 255) * k) | 0;
  const g = Math.min(255, ((n >> 8) & 255) * k) | 0;
  const b = Math.min(255, (n & 255) * k) | 0;
  return 'rgb(' + r + ',' + g + ',' + b + ')';
}
