// Autotile materials: the pure model + deriver behind `.tiles.ts` files (docs/scene2d-editor-plan.md
// §7). A map painted with MATERIAL ids (intent) is turned into atlas tile indices (display) by
// neighbor-mask rules — never the other way around, and the derived indices are never persisted.
//
// Two material kinds, differing only in their neighbor classifier:
// - 'patch' — area materials (grass, dirt, water, platformer ground): the 8-neighbor blob mask.
//   Minimal slot set = fill (+ weighted scatter variants) + 4 edges + 4 outer corners + 4 inner
//   corners; missing slots fall back to simpler ones, so a partial set always renders.
//   `size: 2 | 3` makes the material live on a size×size BLOCK lattice (anchored at map 0,0):
//   classification runs block-to-block, fill holds size² tiles and each edge `size` tiles, while
//   corners stay one cell. That is what art with a multi-cell pattern phase needs (herringbone,
//   lane dashes), and it makes a one-block-wide strip carry BOTH edges by construction — a
//   2-lane road with its curbs and centre line is just `size: 2` painted one block wide.
// - 'path'  — line materials (roads, fences, walls, rails): the 4-neighbor mask, 16 classes
//   (isolated / ends / straights / corners / tees / cross), same fallback principle.
//
// Direction conventions (data is row-major, row 0 = the TOP row): N = row-1, S = row+1, E = col+1,
// W = col-1. Edge arrays are [n, e, s, w]; corner arrays are [ne, se, sw, nw] (clockwise from
// north-east). Cells outside the map count as SAME material, so patches continue off-map instead
// of growing a border along the map edge.
//
// A cell holds a material id, a part id, -1 for empty, or — below -1 — a RAW atlas tile
// (`rawCell(index)`), so hand-placed art can sit in a material map without colliding with ids.
//
// Any atlas index (a raw cell, a slot value) may carry an ORIENTATION packed into the same int:
// `rot90(i)` / `flipX(i)` — see the oriented-tiles section. The deriver treats oriented values as
// opaque positive ints and copies them through; renderers decode the bits into a UV permutation.
//
// Pure and engine-free on purpose — the 2D editor keeps a mirrored copy
// (packages/editor-2d/src/autotile.ts, like cells.ts / history.ts): keep them in sync.

/** A fill entry: an atlas index, or [index, weight] for seeded scatter variants. */
export type FillEntry = number | [number, number]

export type Dir4 = [number, number, number, number]

/** One side's tiles: a single index (used for every cell along that side), or — with `size` > 1 —
 *  the `size` indices ALONG it (n/s run left→right, w/e run top→bottom). */
export type EdgeSlot = number | number[]

/** A whole `size`×`size` piece: one index (drawn in every cell of the block), or size² indices
 *  row-major. Path pieces and parts are blocks — at size 1 they are just a tile. */
export type TileBlock = number | number[]

/** `size`² indices, row-major within the block. */
export type FillBlock = number[]

/** A named element placed BY HAND (a pedestrian crossing in a road, a gate in a fence). It carries
 *  its own `id`, so the map stores it like any material — but it CONNECTS as its parent, so the
 *  run continues straight through it. Never picked at random: you paint it from the palette, ONE
 *  CELL at a time, and the tile is worked out from where that cell sits. */
export type TilePart = {
  /** Stable small int, unique across every material and part of the tileset. */
  id: number
  /** Which run the art is drawn for — 'v' vertical, 'h' horizontal. It also picks the art: the
   *  element varies ACROSS the line (a crossing has one tile per lane) and repeats along it, so
   *  `tiles` is `size` long, indexed by row for 'h' and by column for 'v'. */
  axis?: "v" | "h"
  /** The tiles — one index, `size` across the run when `axis` is set, else the size² block. */
  tiles?: TileBlock
}

/** A block variant: the block, or [block, weight] for seeded scatter. */
export type BlockEntry = FillBlock | [FillBlock, number]

/** One transition set of a patch material — the tiles its border shows against a neighbor. */
export type PatchSlots = {
  /** Edge line tiles [n, e, s, w] — e.g. `edges[0]` draws where the patch stops toward the top. */
  edges?: [EdgeSlot, EdgeSlot, EdgeSlot, EdgeSlot]
  /** Convex corner tiles [ne, se, sw, nw] — the patch's outside corners. Always one cell. */
  outer?: Dir4
  /** Concave corner tiles [ne, se, sw, nw] — the notch where two patch arms meet. One cell. */
  inner?: Dir4
}

export type PatchMaterial = {
  /** Stable small int — what `cells('…')` stores when a map carries Autotile2D. */
  id: number
  kind: "patch"
  /** Editor swatch color ('#4a7c3a'). */
  color?: string
  /** Block size in cells (default 1). With 2 or 3 the material is classified block-to-block on a
   *  lattice anchored at the map origin, `fill` holds size² tiles and each edge `size` tiles. */
  size?: number
  /** Interior tile — one index, or an ARRAY of scatter variants (each `index` or
   *  `[index, weight]`; bare indices weigh 1): `fill: [261, [262, 0.1], [263, 0.05]]`.
   *  With `size` > 1 it is instead the row-major BLOCK of size² indices
   *  (`fill: [130, 131, 146, 147]`), or a list of such blocks for scatter variants. */
  fill: number | FillEntry[] | BlockEntry[]
  /** Transition sets keyed by the neighboring material's name; '*' = any other neighbor
   *  (including empty). Baked packs draw a material fading into a SPECIFIC background. */
  over?: Record<string, PatchSlots>
  /** Hand-placed elements that connect as this material (see TilePart). */
  parts?: Record<string, TilePart>
}

export type PathMaterial = {
  id: number
  kind: "path"
  color?: string
  /** Cells ACROSS the line (default 1). A `size: 2` road is two lanes wide: connectivity runs
   *  block to block and every piece below is a 2×2 block instead of one tile. */
  size?: number
  /** No connections at all. */
  isolated?: TileBlock
  /** End caps [n, e, s, w] — `ends[0]` connects toward the top only. */
  ends?: [TileBlock, TileBlock, TileBlock, TileBlock]
  /** [vertical (n+s), horizontal (e+w)]. */
  straights?: [TileBlock, TileBlock]
  /** Elbows [ne, se, sw, nw] — `corners[0]` connects top + right. */
  corners?: [TileBlock, TileBlock, TileBlock, TileBlock]
  /** T pieces [n, e, s, w] — `tees[0]` is the tee MISSING the top connection. */
  tees?: [TileBlock, TileBlock, TileBlock, TileBlock]
  /** All four connections. */
  cross?: TileBlock
  /** Hand-placed elements that connect as this material (see TilePart). */
  parts?: Record<string, TilePart>
}

export type TileMaterial = PatchMaterial | PathMaterial

export type TilesetDef = {
  /** The atlas image — write `asset('./TileSet.png')`. */
  image: string
  /** Atlas cell size in image pixels — a number (square) or [w, h]. */
  tile: number | [number, number]
  /** Named materials; names are the editor/`over:` vocabulary, `id`s are what maps store. */
  materials?: Record<string, TileMaterial>
}

// ---- helpers -------------------------------------------------------------------

export const normalizeFill = (fill: number | FillEntry[]): [number, number][] => {
  if (typeof fill === "number") return [ [ fill, 1 ] ]
  return fill.map((e) => (typeof e === "number" ? [ e, 1 ] : [ e[0], e[1] ]))
}

/** A material's block size — 1 unless it declares `size` (clamped to a positive int). */
export const blockSize = (mat: TileMaterial): number => Math.max(1, Math.round(mat.size ?? 1))

/** One cell of a block: a scalar covers the whole block, an array reads row-major. Undefined =
 *  unassigned (missing, or the negative sentinel the editor writes for a blank slot). */
export const blockTile = (
  block: TileBlock | undefined, n: number, lx: number, ly: number,
): number | undefined => {
  if (typeof block === "number") return block >= 0 ? block : undefined
  if (!Array.isArray(block)) return undefined
  const t = block[ly * n + lx]
  return typeof t === "number" && t >= 0 ? t : undefined
}

/** How many tiles a part holds: `size` across the run when it declares an axis, else the block. */
export const partSlots = (part: TilePart, n: number): number => (part.axis ? n : n * n)

// ---- raw tiles inside a material map ------------------------------------------------------------
// A material map stores INTENT, so a cell holding a plain atlas index would be ambiguous (is `3`
// material 3 or tile 3?). Raw tiles therefore live below -1, out of the material id space: -2 is
// tile 0, -3 is tile 1, and so on. -1 stays empty, and a raw cell simply draws itself.

/** The cell value that draws atlas tile `index` verbatim in a material map. */
export const rawCell = (index: number): number => -index - 2

/** True for a cell holding a raw atlas tile rather than a material id. */
export const isRawCell = (v: number): boolean => v <= -2

/** The atlas index behind a raw cell (garbage in, garbage out — guard with isRawCell). */
export const rawIndex = (v: number): number => -v - 2

// ---- oriented tiles -----------------------------------------------------------------------------
// Any tile value — a slot in a `.tiles.ts` material, a raw cell, a plain index in a raw map — can
// carry an orientation in the SAME int: bits 0–27 hold the atlas index, bit 28 a horizontal mirror,
// bits 29–30 the clockwise quarter-turns; the mirror applies FIRST, then the rotation. The sign bit
// is never touched, so every oriented value stays positive: -1 (empty), raw cells (≤ -2) and the
// negative "unassigned slot" sentinel all keep working, and the deriver passes oriented values
// through untouched. Renderers permute the atlas cell's UV corners — same quads, same batches.
// `rot90`/`flipX` COMPOSE (they act on the value's current orientation), so `rot90(rot90(t))` is a
// half turn and all 8 orientations of a tile are reachable; write them in `.tiles.ts` files
// directly: `ends: [121, rot90(121), rot180(121), rot270(121)]`.

export const TILE_INDEX_MASK = 0x0fffffff
const TILE_FLIP = 1 << 28

/** The atlas index behind a (possibly oriented) tile value. */
export const tileIndex = (v: number): number => v & TILE_INDEX_MASK
/** Clockwise quarter-turns (0–3) of a tile value. */
export const tileTurns = (v: number): number => (v >> 29) & 3
/** Whether the tile is mirrored horizontally (the mirror applies before the rotation). */
export const tileFlip = (v: number): boolean => (v & TILE_FLIP) !== 0
/** True when the value carries any orientation (renderers keep the fast path otherwise). */
export const tileOriented = (v: number): boolean => v > TILE_INDEX_MASK
/** Build an oriented tile value: `index`, mirrored when `flip`, then `turns` quarter-turns CW. */
export const packTile = (index: number, turns = 0, flip = false): number =>
  (index & TILE_INDEX_MASK) | (flip ? TILE_FLIP : 0) | ((turns & 3) << 29)

/** The value rotated a further 90° clockwise. */
export const rot90 = (v: number): number => packTile(tileIndex(v), tileTurns(v) + 1, tileFlip(v))
/** The value rotated a further 180°. */
export const rot180 = (v: number): number => packTile(tileIndex(v), tileTurns(v) + 2, tileFlip(v))
/** The value rotated a further 270° clockwise (90° counter-clockwise). */
export const rot270 = (v: number): number => packTile(tileIndex(v), tileTurns(v) + 3, tileFlip(v))
/** The value mirrored horizontally (on screen — existing turns are re-based, group math). */
export const flipX = (v: number): number => packTile(tileIndex(v), 4 - tileTurns(v), !tileFlip(v))

/** The tile a part draws in a cell — indexed ACROSS the run ('h' by row, 'v' by column) so one
 *  element covers a lane each and repeats along the run; without an axis it is a plain block. */
export const partTile = (part: TilePart, n: number, lx: number, ly: number): number | undefined => {
  const tiles = part.tiles
  if (typeof tiles === "number") return tiles >= 0 ? tiles : undefined
  if (!Array.isArray(tiles)) return undefined
  const t = tiles[part.axis === "h" ? ly : part.axis === "v" ? lx : ly * n + lx]
  return typeof t === "number" && t >= 0 ? t : undefined
}

/** `fill` as weighted BLOCKS of `size`² indices (row-major). Size 1 keeps the scalar/scatter
 *  reading; with size > 1 a flat number array is one positional block, an array of arrays is the
 *  variant list, and a bare number fills the whole block. Missing entries read as -1. */
export const fillBlocks = (fill: PatchMaterial["fill"], size = 1): [number[], number][] => {
  const n = Math.max(1, Math.round(size))
  const area = n * n
  const pad = (b: readonly number[]): number[] =>
    Array.from({ length: area }, (_, i) => (typeof b[i] === "number" ? b[i]! : -1))
  if (typeof fill === "number") return [ [ new Array<number>(area).fill(fill), 1 ] ]
  if (!Array.isArray(fill) || fill.length === 0) return []
  if (n === 1) return normalizeFill(fill as FillEntry[]).map(([ i, w ]) => [ [ i ], w ])
  if (!Array.isArray(fill[0])) return [ [ pad(fill as number[]), 1 ] ]
  return (fill as BlockEntry[]).map((e): [number[], number] =>
    Array.isArray(e[0]) ? [ pad(e[0] as number[]), (e as [FillBlock, number])[1] ?? 1 ] : [ pad(e as number[]), 1 ])
}

/** Deterministic per-cell hash in [0, 1) — scatter must not reshuffle when nearby cells change. */
const hash01 = (x: number, y: number, seed: number): number => {
  let h = (Math.imul(x, 374761393) + Math.imul(y, 668265263) + Math.imul(seed, 2246822519)) | 0
  h = Math.imul(h ^ (h >>> 13), 1274126177)
  h ^= h >>> 16
  return (h >>> 0) / 4294967296
}

/** The fill tile for block (bc, br) at block-local (lx, ly). Scatter rolls once per BLOCK, so a
 *  multi-cell pattern never tears; at size 1 the block IS the cell (identical to before). */
const pickFill = (
  mat: PatchMaterial, bc: number, br: number, seed: number, n: number, lx: number, ly: number,
): number => {
  const variants = fillBlocks(mat.fill, n)
  if (variants.length === 0) return -1
  const at = ly * n + lx
  if (variants.length === 1) return variants[0]![0][at] ?? -1
  const total = variants.reduce((s, v) => s + v[1], 0)
  let roll = hash01(bc, br, seed + mat.id * 7919) * total
  for (const [ block, w ] of variants) {
    roll -= w
    if (roll < 0) return block[at] ?? -1
  }
  return variants[variants.length - 1]![0][at] ?? -1
}

// corner order [ne, se, sw, nw] as (dx, dy) pairs; edge order [n, e, s, w]
const EDGE_D: [number, number][] = [ [ 0, -1 ], [ 1, 0 ], [ 0, 1 ], [ -1, 0 ] ]
const CORNER_D: [number, number][] = [ [ 1, -1 ], [ 1, 1 ], [ -1, 1 ], [ -1, -1 ] ]

// ---- the deriver ---------------------------------------------------------------

/**
 * Derive display atlas indices from a material-id grid (row-major, row 0 = top, -1 = empty).
 * Values that match no material id pass through unchanged (so converting maps degrades
 * gracefully); -1 stays -1. Deterministic: same inputs + seed → same output on every platform.
 */
export const deriveCells = (
  def: TilesetDef, cols: number, rows: number, cells: ArrayLike<number>, seed = 0,
): Int32Array => {
  const byId = new Map<number, { name: string, mat: TileMaterial }>()
  const parts = new Map<number, { part: TilePart, mat: TileMaterial, owner: number }>()
  for (const [ name, mat ] of Object.entries(def.materials ?? {})) {
    if (!byId.has(mat.id)) byId.set(mat.id, { name, mat })
    for (const part of Object.values(mat.parts ?? {})) {
      if (!parts.has(part.id)) parts.set(part.id, { part, mat, owner: mat.id })
    }
  }
  // a part's cells read as their PARENT everywhere connectivity is asked — the run continues
  // straight through a crossing or a gate; only the drawing differs
  const ownerOf = (v: number): number => parts.get(v)?.owner ?? v

  const out = new Int32Array(cols * rows)
  const at = (c: number, r: number, self: number): number =>
    (c < 0 || r < 0 || c >= cols || r >= rows) ? self : ownerOf(cells[r * cols + c] as number)
  /** A block belongs to `self` when ANY of its cells does — so a block you painted one cell at a
   *  time still reads as part of the run (and off-map keeps counting as same). */
  const blockAt = (ax: number, ay: number, n: number, self: number): number => {
    if (n === 1) return at(ax, ay, self)
    for (let y = ay; y < ay + n; y++) {
      for (let x = ax; x < ax + n; x++) if (at(x, y, self) === self) return self
    }
    return at(ax, ay, self)
  }

  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      const raw = cells[r * cols + c] as number
      // a raw cell draws its atlas tile verbatim — hand-placed art inside a material map
      if (isRawCell(raw)) { out[r * cols + c] = rawIndex(raw); continue }
      const v = raw >= 0 ? ownerOf(raw) : raw
      const entry = v >= 0 ? byId.get(v) : undefined
      if (!entry) { out[r * cols + c] = raw; continue }
      const hit = parts.get(raw)
      if (hit) {
        const n = blockSize(entry.mat)
        const t = partTile(hit.part, n, c - Math.floor(c / n) * n, r - Math.floor(r / n) * n)
        // an unassigned part still draws its material, so a half-authored element never blanks out
        if (t !== undefined) { out[r * cols + c] = t; continue }
      }
      out[r * cols + c] = entry.mat.kind === "patch"
        ? derivePatch(entry.mat, byId, blockAt, c, r, v, seed)
        : derivePath(entry.mat, blockAt, c, r, v)
    }
  }
  return out
}

/** Convex corners as [cardA, cardB, cornerIndex]: n+e → ne(0), e+s → se(1), s+w → sw(2), w+n → nw(3). */
const CORNER_PAIRS: [number, number, number][] = [ [ 0, 1, 0 ], [ 1, 2, 1 ], [ 2, 3, 2 ], [ 3, 0, 3 ] ]

/** Where each concave corner shows inside a block: [localX, localY, cardA, cardB], in [ne, se, sw, nw]. */
const INNER_AT = (n: number): [number, number, number, number][] => [
  [ n - 1, 0, 0, 1 ], [ n - 1, n - 1, 1, 2 ], [ 0, n - 1, 2, 3 ], [ 0, 0, 3, 0 ],
]

/** Samples a whole BLOCK: (anchorX, anchorY, size, self) → the material that block belongs to. */
type BlockAt = (ax: number, ay: number, n: number, self: number) => number

const derivePatch = (
  mat: PatchMaterial, byId: Map<number, { name: string, mat: TileMaterial }>,
  at: BlockAt, c: number, r: number, v: number, seed: number,
): number => {
  // With `size` > 1 the material lives on a block lattice anchored at the map origin: neighbors are
  // sampled one BLOCK away (at that block's anchor cell) and the tile is placed by the cell's
  // position INSIDE the block. n = 1 makes every block one cell — the original per-cell rules.
  const n = blockSize(mat)
  const bc = Math.floor(c / n), br = Math.floor(r / n)
  const lx = c - bc * n, ly = r - br * n
  const ax = bc * n, ay = br * n
  const same = (dx: number, dy: number): boolean => at(ax + dx * n, ay + dy * n, n, v) === v
  // cardinal + diagonal same-bits (diagonals only matter where both adjacent cardinals are same)
  const card = EDGE_D.map(([ dx, dy ]) => same(dx, dy)) // [n, e, s, w]
  const diag = CORNER_D.map(([ dx, dy ]) => same(dx, dy)) // [ne, se, sw, nw]
  const fill = (): number => pickFill(mat, bc, br, seed, n, lx, ly)

  const slots = pickSlots(mat, byId, at, ax, ay, v, n)
  if (!slots) return fill()
  // a negative value inside a slot array means "unassigned" (the editor writes partial sets)
  const pick = (t: number | undefined): number | undefined => (t !== undefined && t >= 0 ? t : undefined)
  const outer = (i: number): number | undefined => pick(slots.outer?.[i])
  const edge = (d: number): number | undefined => {
    const s = slots.edges?.[d]
    if (typeof s === "number") return pick(s)
    return Array.isArray(s) ? pick(s[d === 0 || d === 2 ? lx : ly]) : undefined
  }

  // sides of THIS cell that sit on a block border facing another material
  const on = [
    !card[0]! && ly === 0, !card[1]! && lx === n - 1, !card[2]! && ly === n - 1, !card[3]! && lx === 0,
  ]
  for (const [ a, b, i ] of CORNER_PAIRS) {
    if (on[a]! && on[b]!) { const t = outer(i); if (t !== undefined) return t }
  }
  for (let d = 0; d < 4; d++) if (on[d]!) { const t = edge(d); if (t !== undefined) return t }
  if (on.some(Boolean)) return fill()

  // block interior — a differing relevant diagonal is a concave (inner) corner, drawn in the one
  // cell of the block that touches it
  const inner = INNER_AT(n)
  for (let i = 0; i < 4; i++) {
    const [ px, py, a, b ] = inner[i]!
    if (lx === px && ly === py && card[a]! && card[b]! && !diag[i]!) return pick(slots.inner?.[i]) ?? fill()
  }
  return fill()
}

/** The transition set for this block: keyed by the dominant differing neighbor's material name,
 *  '*' as the fallback (empty and unknown neighbors always land on '*'). */
const pickSlots = (
  mat: PatchMaterial, byId: Map<number, { name: string, mat: TileMaterial }>,
  at: BlockAt, ax: number, ay: number, v: number, n: number,
): PatchSlots | undefined => {
  const over = mat.over
  if (!over) return undefined
  const counts = new Map<number, number>()
  for (const [ dx, dy ] of [ ...EDGE_D, ...CORNER_D ]) {
    const nv = at(ax + dx * n, ay + dy * n, n, v)
    if (nv !== v) counts.set(nv, (counts.get(nv) ?? 0) + 1)
  }
  let bestId: number | null = null, best = 0
  for (const [ id, n ] of counts) {
    if (n > best || (n === best && bestId !== null && id < bestId)) { best = n; bestId = id }
  }
  const name = bestId !== null && bestId >= 0 ? byId.get(bestId)?.name : undefined
  return (name !== undefined ? over[name] : undefined) ?? over["*"]
}

const derivePath = (
  mat: PathMaterial, at: BlockAt, c: number, r: number, v: number,
): number => {
  // `size` makes the line that many cells ACROSS: connectivity is block to block and every piece
  // is a size² block placed by the cell's position inside it (size 1 = one tile per piece).
  const n = blockSize(mat)
  const bc = Math.floor(c / n), br = Math.floor(r / n)
  const lx = c - bc * n, ly = r - br * n
  const ax = bc * n, ay = br * n
  const conn = EDGE_D.map(([ dx, dy ]) => at(ax + dx * n, ay + dy * n, n, v) === v) // [n, e, s, w]
  const links = conn.filter(Boolean).length
  // a negative value inside a piece means "unassigned" (the editor writes partial sets)
  const pick = (b: TileBlock | undefined): number | undefined => blockTile(b, n, lx, ly)
  const any = (): number =>
    pick(mat.isolated) ?? pick(mat.cross) ?? pick(mat.straights?.[1]) ?? pick(mat.straights?.[0])
    ?? mat.ends?.map((b) => pick(b)).find((t) => t !== undefined)
    ?? pick(mat.corners?.[0]) ?? pick(mat.tees?.[0]) ?? v

  if (links === 0) return pick(mat.isolated) ?? any()
  if (links === 1) {
    const d = conn.findIndex(Boolean)
    return pick(mat.ends?.[d]) ?? pick(mat.straights?.[d % 2 === 0 ? 0 : 1]) ?? any()
  }
  if (links === 2) {
    if (conn[0] && conn[2]) return pick(mat.straights?.[0]) ?? any()
    if (conn[1] && conn[3]) return pick(mat.straights?.[1]) ?? any()
    // elbows: n+e → ne(0), e+s → se(1), s+w → sw(2), w+n → nw(3)
    const corner = conn[0] && conn[1] ? 0 : conn[1] && conn[2] ? 1 : conn[2] && conn[3] ? 2 : 3
    return pick(mat.corners?.[corner]) ?? pick(mat.cross) ?? any()
  }
  if (links === 3) {
    const missing = conn.findIndex((s) => !s)
    return pick(mat.tees?.[missing]) ?? pick(mat.cross) ?? pick(mat.straights?.[missing % 2 === 0 ? 1 : 0]) ?? any()
  }
  return pick(mat.cross) ?? any()
}
