/**
 * Terrain and props around server-defined towns and roads.
 *
 * 1) Town centers + radii define settlements (plaza + rim).
 * 2) A closed ring through towns (sorted by levelMin) defines the main road corridor.
 * 3) Optional `activeRoute` waypoints from `route_assigned` refine the path for the current leg.
 * 4) Wild tiles use the nearest town's biome as the base, then light noise.
 */

/** One town for terrain influence (from REST /towns or TownData). */
export interface TownTerrainInfluence {
  id: number;
  cx: number;
  cy: number;
  radius: number;
  biome: string;
  levelMin: number;
}

/** Built once per frame batch / when towns or route change. */
export interface WorldTerrainContext {
  towns: TownTerrainInfluence[];
  /** Town centers in travel order (sorted by levelMin), for the ring road. */
  networkCenters: Array<{ x: number; y: number }>;
  /** Current road polyline from server (may be empty). */
  activeRoute: Array<{ x: number; y: number }> | null;
}

/** Deterministic integer hash for tile coordinates -> [0, 1) */
export function tileHash(x: number, y: number, seed: number): number {
  let h = (Math.imul(x | 0, 374761393) + Math.imul(y | 0, 668265263) + seed) | 0;
  h = Math.imul(h ^ (h >>> 13), 1274126177);
  h = h ^ (h >>> 16);
  return (h & 0x7fffffff) / 0x7fffffff;
}

function distPointToSegment(
  px: number,
  py: number,
  ax: number,
  ay: number,
  bx: number,
  by: number,
): number {
  const abx = bx - ax;
  const aby = by - ay;
  const apx = px - ax;
  const apy = py - ay;
  const ab2 = abx * abx + aby * aby;
  let t = ab2 > 1e-12 ? (apx * abx + apy * aby) / ab2 : 0;
  t = Math.max(0, Math.min(1, t));
  const qx = ax + abx * t;
  const qy = ay + aby * t;
  return Math.hypot(px - qx, py - qy);
}

/** Open polyline: consecutive vertex pairs. */
export function minDistToOpenPolyline(
  px: number,
  py: number,
  pts: Array<{ x: number; y: number }>,
): number {
  if (pts.length < 2) return Number.POSITIVE_INFINITY;
  let min = Number.POSITIVE_INFINITY;
  for (let i = 0; i < pts.length - 1; i++) {
    const a = pts[i]!;
    const b = pts[i + 1]!;
    const d = distPointToSegment(px, py, a.x, a.y, b.x, b.y);
    if (d < min) min = d;
  }
  return min;
}

/** Closed ring through centers: (c0,c1)…(c_{n-2},c_{n-1}),(c_{n-1},c0). */
export function minDistToRing(
  px: number,
  py: number,
  centers: Array<{ x: number; y: number }>,
): number {
  const n = centers.length;
  if (n < 2) return Number.POSITIVE_INFINITY;
  let min = Number.POSITIVE_INFINITY;
  for (let i = 0; i < n; i++) {
    const a = centers[i]!;
    const b = centers[(i + 1) % n]!;
    const d = distPointToSegment(px, py, a.x, a.y, b.x, b.y);
    if (d < min) min = d;
  }
  return min;
}

/** Sort towns like the server chain (level_min, then id). */
export function orderedTownCenters(towns: TownTerrainInfluence[]): Array<{ x: number; y: number }> {
  const sorted = [...towns].sort((a, b) => a.levelMin - b.levelMin || a.id - b.id);
  return sorted.map((t) => ({ x: t.cx, y: t.cy }));
}

export function buildWorldTerrainContext(
  towns: TownTerrainInfluence[],
  activeRoute: Array<{ x: number; y: number }> | null,
): WorldTerrainContext {
  return {
    towns,
    networkCenters: orderedTownCenters(towns),
    activeRoute: activeRoute && activeRoute.length >= 2 ? activeRoute : null,
  };
}

/** Helpers for Minimap / tests: Town[] -> influences */
export function townsApiToInfluences(
  towns: Array<{
    id: number;
    worldX: number;
    worldY: number;
    radius: number;
    biome: string;
    levelMin: number;
  }>,
): TownTerrainInfluence[] {
  return towns.map((t) => ({
    id: t.id,
    cx: t.worldX,
    cy: t.worldY,
    radius: t.radius,
    biome: t.biome,
    levelMin: t.levelMin,
  }));
}

function roadClearance(wx: number, wy: number, ctx: WorldTerrainContext): number {
  const ringD = minDistToRing(wx, wy, ctx.networkCenters);
  const activeD = ctx.activeRoute
    ? minDistToOpenPolyline(wx, wy, ctx.activeRoute)
    : Number.POSITIVE_INFINITY;
  return Math.min(ringD, activeD);
}

function nearestTown(wx: number, wy: number, towns: TownTerrainInfluence[]): TownTerrainInfluence | null {
  if (towns.length === 0) return null;
  let best = towns[0]!;
  let bestD = Math.hypot(wx - best.cx, wy - best.cy);
  for (let i = 1; i < towns.length; i++) {
    const t = towns[i]!;
    const d = Math.hypot(wx - t.cx, wy - t.cy);
    if (d < bestD) {
      bestD = d;
      best = t;
    }
  }
  return best;
}

/** Map server biome id to a ground tile key (colors in renderer). */
export function biomeToTerrainKey(biome: string): string {
  const b = biome.toLowerCase();
  if (b === 'forest') return 'forest_floor';
  if (b === 'ruins') return 'ruins_floor';
  if (b === 'canyon') return 'canyon_floor';
  if (b === 'swamp') return 'swamp_floor';
  if (b === 'volcanic') return 'volcanic_floor';
  if (b === 'astral') return 'astral_floor';
  if (b === 'meadow') return 'grass';
  return 'grass';
}

/**
 * Terrain at integer tile (wx, wy).
 * With `context`, roads follow the town ring + active route; wild tiles follow nearest town biome.
 * Without context, simple grass + noise (no synthetic diagonal road).
 */
export function proceduralTerrain(
  wx: number,
  wy: number,
  context?: WorldTerrainContext | null,
): string {
  const ctx = context;
  if (!ctx || ctx.towns.length === 0) {
    const h = tileHash(wx, wy, 42);
    if (h < 0.06) return 'dirt';
    if (h < 0.1) return 'stone';
    return 'grass';
  }

  let minEdge = Number.POSITIVE_INFINITY;
  for (const t of ctx.towns) {
    const d = Math.hypot(wx - t.cx, wy - t.cy) - t.radius;
    if (d < minEdge) minEdge = d;
  }

  if (minEdge < -0.8) return 'plaza';
  if (minEdge < 2.2) return 'dirt';

  const rd = roadClearance(wx, wy, ctx);
  if (rd < 1.15) return 'road';
  if (rd < 2.65) return 'dirt';

  const near = nearestTown(wx, wy, ctx.towns);
  let base = near ? biomeToTerrainKey(near.biome) : 'grass';
  const h = tileHash(wx, wy, 42);
  if (base === 'forest_floor' && h < 0.12) return 'dirt';
  if (base === 'swamp_floor' && h < 0.1) return 'dirt';
  if (h < 0.04) return 'stone';
  if (h < 0.09) return 'dirt';
  return base;
}

/**
 * Compute the distance from the nearest town edge (negative = inside town).
 * Returns Infinity if no towns.
 */
function townEdgeDist(wx: number, wy: number, towns: TownTerrainInfluence[]): number {
  let minEdge = Number.POSITIVE_INFINITY;
  for (const t of towns) {
    const d = Math.hypot(wx - t.cx, wy - t.cy) - t.radius;
    if (d < minEdge) minEdge = d;
  }
  return minEdge;
}

/**
 * Cluster-based noise: tiles near a "cluster seed" tile are more likely to share objects.
 * Returns a hash that correlates with nearby tiles (spatial coherence ~3 tiles).
 */
function clusterHash(wx: number, wy: number, seed: number): number {
  const cx = Math.floor(wx / 3);
  const cy = Math.floor(wy / 3);
  return tileHash(cx, cy, seed);
}

/**
 * Prop types for ground layer.
 * Uses zoning: plaza (sparse, server provides buildings), town edge (fences/barrels),
 * road buffer (clear), wild (clustered natural objects).
 */
export function proceduralObject(
  wx: number,
  wy: number,
  terrain: string,
  context?: WorldTerrainContext | null,
): string | null {
  // Inside plaza: no procedural props (server buildings handle this)
  if (terrain === 'plaza') {
    return null;
  }

  const ctx = context;
  const rd = ctx ? roadClearance(wx, wy, ctx) : Number.POSITIVE_INFINITY;

  // Road buffer: keep clear for passage
  if (rd < 3.0) return null;

  // Town edge zone: sparse village decor (no trees/ruins)
  if (ctx && ctx.towns.length > 0) {
    const edge = townEdgeDist(wx, wy, ctx.towns);
    if (edge >= -2.0 && edge < 5.0) {
      const h = tileHash(wx, wy, 301);
      if (h < 0.04) return 'barrel';
      if (h < 0.065) return 'cart';
      if (h < 0.085) return 'bush';
      if (h < 0.095) return 'leaves';
      return null;
    }
  }

  // Wild zone: cluster-based natural objects for spatial coherence
  const h = tileHash(wx, wy, 137);
  const ch = clusterHash(wx, wy, 137);

  // Trees appear in groves (cluster hash controls grove probability)
  let treeTh = ch < 0.3 ? 0.08 : 0.02;
  if (terrain === 'forest_floor') treeTh = ch < 0.4 ? 0.14 : 0.03;
  if (terrain === 'swamp_floor') treeTh = ch < 0.25 ? 0.06 : 0.01;
  if (h < treeTh) return 'tree';

  // Bushes cluster around trees
  const bushTh = ch < 0.35 ? 0.06 : 0.02;
  if (h < treeTh + bushTh) return 'bush';

  // Rocks appear in rocky patches
  const rockCh = clusterHash(wx, wy, 241);
  if (rockCh < 0.2 && h < treeTh + bushTh + 0.04) return 'rock';

  // Sparse scattered objects (less chaotic than before)
  const sparse = tileHash(wx, wy, 199);
  if (sparse < 0.008) return 'stump';
  if (sparse < 0.014) return 'cart';
  if (sparse < 0.018) return 'bones';
  if (sparse < 0.024) return 'mushroom';
  if (sparse < 0.028) return 'leaves';
  if (terrain === 'ruins_floor' && sparse < 0.04) return 'ruin';

  return null;
}

/** Blocking object types that hero cannot walk through */
const BLOCKING_TYPES = new Set(['tree', 'bush', 'rock', 'ruin', 'stall', 'well', 'barrel']);

/**
 * Check if a tile at the given world coordinate is blocked by a procedural obstacle.
 */
export function isProcedurallyBlocked(
  wx: number,
  wy: number,
  context?: WorldTerrainContext | null,
): boolean {
  const ix = Math.floor(wx);
  const iy = Math.floor(wy);
  const terrain = proceduralTerrain(ix, iy, context);
  const obj = proceduralObject(ix, iy, terrain, context);
  if (obj === null) return false;
  return BLOCKING_TYPES.has(obj);
}
