/**
 * Warehouse Builder — pure geometry: bay slicing, access points, rotation, walls,
 * collision, zone lookup. Metres, y-down. DOM-free (shared with the server).
 *
 * Local frame of a shelf: length along local +x (bay 0 at local x = 0), "front" = local +y
 * (the bottom edge at rotation 0). `rotation` is clockwise in SVG coordinates:
 *   0   → local +x = world +x (bay 0 left),   front = S
 *   90  → local +x = world +y (bay 0 top),    front = W
 *   180 → local +x = world −x (bay 0 right),  front = N
 *   270 → local +x = world −y (bay 0 bottom), front = E
 */
import type { WbAccess, WbLayout, WbObject, WbPallet, WbRotation, WbShelf, WbSide, WbWall, WbZone, WbCamera } from './schema'
import { WB_SNAP, isBlockingType, round2, snap } from './schema'

export const SIDES: WbSide[] = ['N', 'E', 'S', 'W']

export const sideVector = (side: WbSide): { dx: number; dy: number } =>
  side === 'N' ? { dx: 0, dy: -1 } : side === 'E' ? { dx: 1, dy: 0 } : side === 'S' ? { dx: 0, dy: 1 } : { dx: -1, dy: 0 }

export const worldSide = (access: WbAccess, rotation: WbRotation): WbSide => {
  const local = access === 'front' ? 2 : 0 // S / N
  return SIDES[(local + rotation / 90) % 4]
}

export const lengthAxis = (rotation: WbRotation): 'x' | 'y' => (rotation === 0 || rotation === 180 ? 'x' : 'y')

export interface WbBay {
  key: string            // `${objectKey}#${index}`
  objectKey: string
  objectType: 'shelf' | 'pallet'
  index: number
  code: string           // Y code (pallet: '')
  x: number; y: number; w: number; d: number
  cx: number; cy: number
  side: WbSide
  access: { x: number; y: number }
  levels: string[]       // Z codes (pallet: [])
  skip?: boolean
}

/** Y codes of a bay spec: explicit list, numeric range (zero padded) or letter stepping. */
export function bayCodes (spec: WbShelf['bays']): string[] {
  const n = Math.max(1, Math.round(spec.count || 1))
  if (Array.isArray(spec.codes) && spec.codes.length) {
    const out = spec.codes.slice(0, n).map(String)
    while (out.length < n) out.push(String(out.length + 1).padStart(spec.pad || 2, '0'))
    return spec.direction === 'desc' ? out.reverse() : out
  }
  const start = String(spec.start ?? '01').trim()
  const step = Math.max(1, Math.round(spec.step || 1))
  const out: string[] = []
  if (/^\d+$/.test(start)) {
    const pad = Math.max(spec.pad || 0, start.length)
    const s = parseInt(start, 10)
    for (let i = 0; i < n; i++) out.push(String(s + i * step).padStart(pad, '0'))
  } else if (/^[A-Za-z]$/.test(start)) {
    const s = start.toUpperCase().charCodeAt(0)
    for (let i = 0; i < n; i++) {
      const c = s + i * step
      out.push(c <= 90 ? String.fromCharCode(c) : String.fromCharCode(65 + Math.floor((c - 65) / 26) - 1) + String.fromCharCode(65 + ((c - 65) % 26)))
    }
  } else {
    // alphanumeric like "A1": keep the alpha prefix, step the numeric tail
    const m = start.match(/^(.*?)(\d+)$/)
    if (m) {
      const pad = m[2].length
      const s = parseInt(m[2], 10)
      for (let i = 0; i < n; i++) out.push(m[1] + String(s + i * step).padStart(pad, '0'))
    } else {
      for (let i = 0; i < n; i++) out.push(i === 0 ? start : start + (i + 1))
    }
  }
  return spec.direction === 'desc' ? out.reverse() : out
}

const accessPoint = (cx: number, cy: number, halfDepth: number, side: WbSide) => {
  const v = sideVector(side)
  return { x: round2(cx + v.dx * (halfDepth + 0.5)), y: round2(cy + v.dy * (halfDepth + 0.5)) }
}

export function shelfBays (s: WbShelf): WbBay[] {
  const codes = bayCodes(s.bays)
  const n = codes.length
  const axis = lengthAxis(s.rotation)
  const side = worldSide(s.accessSide, s.rotation)
  const reversed = s.rotation === 180 || s.rotation === 270
  const out: WbBay[] = []
  for (let i = 0; i < n; i++) {
    const slot = reversed ? n - 1 - i : i
    let bx = s.x, by = s.y, bw = s.w, bd = s.d
    if (axis === 'x') { bw = s.w / n; bx = s.x + slot * bw } else { bd = s.d / n; by = s.y + slot * bd }
    const cx = bx + bw / 2, cy = by + bd / 2
    const halfDepth = axis === 'x' ? s.d / 2 : s.w / 2
    const pb = s.perBay?.[i]
    out.push({
      key: `${s.key}#${i}`, objectKey: s.key, objectType: 'shelf', index: i, code: codes[i],
      x: round2(bx), y: round2(by), w: round2(bw), d: round2(bd), cx: round2(cx), cy: round2(cy),
      side, access: accessPoint(cx, cy, halfDepth, side),
      levels: pb?.levels?.length ? pb.levels : s.levels,
      ...(pb?.skip ? { skip: true } : {})
    })
  }
  return out
}

export function palletBay (p: WbPallet): WbBay {
  const side = worldSide(p.accessSide, p.rotation)
  const cx = p.x + p.w / 2, cy = p.y + p.d / 2
  const halfDepth = (side === 'N' || side === 'S') ? p.d / 2 : p.w / 2
  return { key: `${p.key}#0`, objectKey: p.key, objectType: 'pallet', index: 0, code: '', x: p.x, y: p.y, w: p.w, d: p.d, cx: round2(cx), cy: round2(cy), side, access: accessPoint(cx, cy, halfDepth, side), levels: [] }
}

/** All bays of a layout (shelves + pallets), in object order. */
export function layoutBays (layout: WbLayout): WbBay[] {
  const out: WbBay[] = []
  for (const o of layout.objects) {
    if (o.type === 'shelf') out.push(...shelfBays(o))
    else if (o.type === 'pallet') out.push(palletBay(o))
  }
  return out
}

// ---- rects / collision ---------------------------------------------------------------------
export interface Rect { x: number; y: number; w: number; d: number }

export const rectsIntersect = (a: Rect, b: Rect, eps = 1e-6): boolean =>
  a.x < b.x + b.w - eps && a.x + a.w > b.x + eps && a.y < b.y + b.d - eps && a.y + a.d > b.y + eps

export const pointInRect = (px: number, py: number, r: Rect): boolean =>
  px >= r.x && px <= r.x + r.w && py >= r.y && py <= r.y + r.d

/** Key of the first blocking object overlapping `rect` (ignoring `selfKeys`), or null. */
export function findCollision (rect: Rect, objects: WbObject[], selfKeys: Set<string> | string[] = [], onlyIfBlocking = true): string | null {
  const self = selfKeys instanceof Set ? selfKeys : new Set(selfKeys)
  for (const o of objects) {
    if (self.has(o.key)) continue
    if (onlyIfBlocking && !isBlockingType(o.type)) continue
    if (rectsIntersect(rect, o)) return o.key
  }
  return null
}

export const insideGrid = (r: Rect, grid: { width: number; height: number }): boolean =>
  r.x >= 0 && r.y >= 0 && r.x + r.w <= grid.width + 1e-6 && r.y + r.d <= grid.height + 1e-6

/** Rotate by ±90°: swap w/d around the centre, re-snap the top-left to the grid. */
export function rotatedPatch (o: WbPlacedLike, dir: 1 | -1 = 1): { x: number; y: number; w: number; d: number; rotation: WbRotation } {
  const rotation = (((o.rotation + dir * 90) % 360) + 360) % 360 as WbRotation
  const cx = o.x + o.w / 2, cy = o.y + o.d / 2
  const w = o.d, d = o.w
  return { x: snap(cx - w / 2), y: snap(cy - d / 2), w, d, rotation }
}
type WbPlacedLike = { x: number; y: number; w: number; d: number; rotation: WbRotation }

// ---- walls -----------------------------------------------------------------------------------
export const normalizeWall = (w: WbWall): WbWall => {
  const [x1, y1, x2, y2] = w.map(round2)
  if (x1 === x2) return [x1, Math.min(y1, y2), x2, Math.max(y1, y2)]
  return [Math.min(x1, x2), y1, Math.max(x1, x2), y2]
}

/** Lock a dragged wall to the dominant axis and the 0.5 grid. Returns null for zero length. */
export function orthoWall (x1: number, y1: number, x2: number, y2: number): WbWall | null {
  const sx = snap(x1), sy = snap(y1)
  let ex = snap(x2), ey = snap(y2)
  if (Math.abs(ex - sx) >= Math.abs(ey - sy)) ey = sy; else ex = sx
  if (ex === sx && ey === sy) return null
  return normalizeWall([sx, sy, ex, ey])
}

/** Append a wall, merging collinear segments that touch or overlap. */
export function mergeWall (walls: WbWall[], wall: WbWall): WbWall[] {
  let cur = normalizeWall(wall)
  const vertical = cur[0] === cur[2]
  const rest: WbWall[] = []
  for (const w of walls) {
    const wv = w[0] === w[2]
    const sameLine = vertical ? (wv && w[0] === cur[0]) : (!wv && w[1] === cur[1])
    if (!sameLine) { rest.push(w); continue }
    const [a1, a2] = vertical ? [cur[1], cur[3]] : [cur[0], cur[2]]
    const [b1, b2] = vertical ? [w[1], w[3]] : [w[0], w[2]]
    if (a2 >= b1 && b2 >= a1) {
      const lo = Math.min(a1, b1), hi = Math.max(a2, b2)
      cur = vertical ? [cur[0], lo, cur[0], hi] : [lo, cur[1], hi, cur[1]]
    } else rest.push(w)
  }
  rest.push(cur)
  return rest
}

export const roomWalls = (r: Rect): WbWall[] => {
  const x1 = snap(r.x), y1 = snap(r.y), x2 = snap(r.x + r.w), y2 = snap(r.y + r.d)
  if (x1 === x2 || y1 === y2) return []
  return [[x1, y1, x2, y1], [x2, y1, x2, y2], [x1, y2, x2, y2], [x1, y1, x1, y2]]
}

/** Distance from a point to a wall segment. */
export function wallDistance (w: WbWall, px: number, py: number): number {
  const [x1, y1, x2, y2] = w
  const dx = x2 - x1, dy = y2 - y1
  const len2 = dx * dx + dy * dy || 1
  const t = Math.max(0, Math.min(1, ((px - x1) * dx + (py - y1) * dy) / len2))
  const qx = x1 + t * dx, qy = y1 + t * dy
  return Math.hypot(px - qx, py - qy)
}

export function wallIndexAt (walls: WbWall[], px: number, py: number, tol = 0.3): number {
  let best = -1, bestD = tol
  walls.forEach((w, i) => { const d = wallDistance(w, px, py); if (d <= bestD) { best = i; bestD = d } })
  return best
}

/** Does `rect` cross a wall? (wall strictly inside the rect interior, not on its border) */
export function rectCrossesWall (r: Rect, walls: WbWall[], eps = 0.01): boolean {
  for (const [x1, y1, x2, y2] of walls) {
    if (x1 === x2) { // vertical
      if (x1 > r.x + eps && x1 < r.x + r.w - eps && y1 < r.y + r.d - eps && y2 > r.y + eps) return true
    } else if (y1 > r.y + eps && y1 < r.y + r.d - eps && x1 < r.x + r.w - eps && x2 > r.x + eps) return true
  }
  return false
}

// ---- zones -------------------------------------------------------------------------------------
/** Zone containing the point: higher priority first, then the smaller area. */
export function zoneAt (zones: WbZone[], px: number, py: number): WbZone | null {
  let best: WbZone | null = null
  for (const z of zones) {
    if (!pointInRect(px, py, z)) continue
    if (!best) { best = z; continue }
    const pb = best.priority || 0, pz = z.priority || 0
    if (pz > pb || (pz === pb && z.w * z.d < best.w * best.d)) best = z
  }
  return best
}

/** Zone code per bay key (object override wins; `null` override = none). */
export function zoneMembership (layout: WbLayout, bays: WbBay[]): Map<string, string | null> {
  const byKey = new Map<string, WbObject>(layout.objects.map(o => [o.key, o]))
  const out = new Map<string, string | null>()
  for (const b of bays) {
    const o = byKey.get(b.objectKey) as WbShelf | WbPallet | undefined
    if (o && o.zoneOverride !== undefined) { out.set(b.key, o.zoneOverride); continue }
    out.set(b.key, zoneAt(layout.zones, b.cx, b.cy)?.code ?? null)
  }
  return out
}

// ---- misc ---------------------------------------------------------------------------------------
/** Next unused single/double letter rack code (A…Z, AA…). */
export function nextRackCode (layout: WbLayout): string {
  const used = new Set(layout.objects.filter(o => o.type === 'shelf').map(o => (o as WbShelf).rackCode.toUpperCase()))
  for (let i = 0; i < 26 * 27; i++) {
    const code = i < 26 ? String.fromCharCode(65 + i) : String.fromCharCode(65 + Math.floor(i / 26) - 1) + String.fromCharCode(65 + (i % 26))
    if (!used.has(code)) return code
  }
  return 'X' + (layout.objects.length + 1)
}

export const bboxOf = (items: Rect[]): Rect | null => {
  if (!items.length) return null
  let x1 = Infinity, y1 = Infinity, x2 = -Infinity, y2 = -Infinity
  for (const r of items) { x1 = Math.min(x1, r.x); y1 = Math.min(y1, r.y); x2 = Math.max(x2, r.x + r.w); y2 = Math.max(y2, r.y + r.d) }
  return { x: x1, y: y1, w: x2 - x1, d: y2 - y1 }
}

export { WB_SNAP }

// ---- magnetic snapping (walls, object edges, zone edges, hall boundary) ---------------------------------
/** A straight edge something can snap to: `axis: 'x'` = a vertical line at x = v spanning y ∈ [lo, hi]. */
export interface SnapLine { axis: 'x' | 'y'; v: number; lo: number; hi: number; src: string }
const edgesOf = (r: Rect, src: string): SnapLine[] => [
  { axis: 'x', v: r.x, lo: r.y, hi: r.y + r.d, src }, { axis: 'x', v: r.x + r.w, lo: r.y, hi: r.y + r.d, src },
  { axis: 'y', v: r.y, lo: r.x, hi: r.x + r.w, src }, { axis: 'y', v: r.y + r.d, lo: r.x, hi: r.x + r.w, src }
]
/** Every edge in the plan except the excluded keys (the things being moved). */
export function snapLines (layout: { grid: { width: number; height: number }; walls: WbWall[]; objects: WbObject[]; zones: WbZone[] }, exclude: Set<string> | string[] = []): SnapLine[] {
  const ex = exclude instanceof Set ? exclude : new Set(exclude)
  const out: SnapLine[] = []
  const g = layout.grid
  out.push({ axis: 'x', v: 0, lo: 0, hi: g.height, src: 'grid' }, { axis: 'x', v: g.width, lo: 0, hi: g.height, src: 'grid' }, { axis: 'y', v: 0, lo: 0, hi: g.width, src: 'grid' }, { axis: 'y', v: g.height, lo: 0, hi: g.width, src: 'grid' })
  for (const [x1, y1, x2, y2] of layout.walls) {
    if (x1 === x2) out.push({ axis: 'x', v: x1, lo: Math.min(y1, y2), hi: Math.max(y1, y2), src: 'wall' })
    else out.push({ axis: 'y', v: y1, lo: Math.min(x1, x2), hi: Math.max(x1, x2), src: 'wall' })
  }
  for (const o of layout.objects) if (!ex.has(o.key)) out.push(...edgesOf(o, 'object'))
  for (const z of layout.zones) if (!ex.has(z.key)) out.push(...edgesOf(z, 'zone'))
  return out
}
/** Nearest line on `axis` within `tol` of `v` whose span overlaps [lo, hi] (± tol). */
export function snapCoord (lines: SnapLine[], axis: 'x' | 'y', v: number, lo: number, hi: number, tol: number): SnapLine | null {
  let best: SnapLine | null = null, bd = tol
  for (const l of lines) {
    if (l.axis !== axis || l.lo - tol > hi || l.hi + tol < lo) continue
    const d = Math.abs(l.v - v)
    if (d < bd) { bd = d; best = l }
  }
  return best
}
/** Snap a free point (drawing): each coordinate independently. */
export function snapPoint (lines: SnapLine[], x: number, y: number, tol: number): { x: number; y: number; guides: SnapLine[] } {
  const gx = snapCoord(lines, 'x', x, y, y, tol), gy = snapCoord(lines, 'y', y, x, x, tol)
  return { x: gx ? gx.v : x, y: gy ? gy.v : y, guides: [gx, gy].filter(Boolean) as SnapLine[] }
}
/** Snap a rect that is being moved: whichever of its two edges per axis is closest to a line wins. */
export function snapRectMove (lines: SnapLine[], r: Rect, tol: number): { dx: number; dy: number; guides: SnapLine[] } {
  const pick = (axis: 'x' | 'y', a: number, b: number, lo: number, hi: number) => {
    const la = snapCoord(lines, axis, a, lo, hi, tol), lb = snapCoord(lines, axis, b, lo, hi, tol)
    const da = la ? Math.abs(la.v - a) : Infinity, db = lb ? Math.abs(lb.v - b) : Infinity
    if (da === Infinity && db === Infinity) return { delta: 0, line: null as SnapLine | null }
    return da <= db ? { delta: la!.v - a, line: la } : { delta: lb!.v - b, line: lb }
  }
  const sx = pick('x', r.x, r.x + r.w, r.y, r.y + r.d), sy = pick('y', r.y, r.y + r.d, r.x, r.x + r.w)
  return { dx: sx.delta, dy: sy.delta, guides: [sx.line, sy.line].filter(Boolean) as SnapLine[] }
}

// ---- camera coverage (ray-cast view cone, shadowed by walls, shelves and obstacles) ---------------------------
export interface CoveragePoly { key: string; origin: [number, number]; points: Array<[number, number]>; area: number }
type Seg = [number, number, number, number]
const rayHit = (ox: number, oy: number, dx: number, dy: number, s: Seg): number | null => {
  const ex = s[2] - s[0], ey = s[3] - s[1]
  const den = dx * ey - dy * ex
  if (Math.abs(den) < 1e-9) return null
  const fx = s[0] - ox, fy = s[1] - oy
  const t = (fx * ey - fy * ex) / den
  const u = (fx * dy - fy * dx) / den
  return t > 1e-6 && u >= -1e-6 && u <= 1 + 1e-6 ? t : null
}
export function polygonArea (pts: Array<[number, number]>): number {
  let a = 0
  for (let i = 0, n = pts.length; i < n; i++) { const [x1, y1] = pts[i], [x2, y2] = pts[(i + 1) % n]; a += x1 * y2 - x2 * y1 }
  return Math.abs(a) / 2
}
/** What a camera sees from its centre: one ray per `stepDeg` across the opening angle, stopped by walls, shelves,
 *  obstacles, the hall boundary or the range. Heading 0 = north (−y), clockwise. */
export function cameraCoverage (cam: WbCamera, layout: { grid: { width: number; height: number }; walls: WbWall[]; objects: WbObject[] }, stepDeg = 2): CoveragePoly {
  const ox = cam.x + cam.w / 2, oy = cam.y + cam.d / 2
  const segs: Seg[] = [...layout.walls]
  const g = layout.grid
  segs.push([0, 0, g.width, 0], [g.width, 0, g.width, g.height], [g.width, g.height, 0, g.height], [0, g.height, 0, 0])
  for (const o of layout.objects) {
    if (o.key === cam.key || (o.type !== 'shelf' && o.type !== 'obstacle')) continue
    segs.push([o.x, o.y, o.x + o.w, o.y], [o.x + o.w, o.y, o.x + o.w, o.y + o.d], [o.x + o.w, o.y + o.d, o.x, o.y + o.d], [o.x, o.y + o.d, o.x, o.y])
  }
  const fov = Math.min(360, Math.max(1, cam.fov)), range = Math.max(0.5, cam.range)
  const n = Math.max(8, Math.ceil(fov / stepDeg))
  const pts: Array<[number, number]> = []
  for (let i = 0; i <= n; i++) {
    const a = ((cam.heading - fov / 2 + (fov * i) / n) * Math.PI) / 180
    const dx = Math.sin(a), dy = -Math.cos(a)
    let t = range
    for (const s of segs) { const h = rayHit(ox, oy, dx, dy, s); if (h !== null && h < t) t = h }
    pts.push([Math.round((ox + dx * t) * 100) / 100, Math.round((oy + dy * t) * 100) / 100])
  }
  const points: Array<[number, number]> = fov >= 360 ? pts : [[Math.round(ox * 100) / 100, Math.round(oy * 100) / 100], ...pts]
  return { key: cam.key, origin: [ox, oy], points, area: Math.round(polygonArea(points) * 100) / 100 }
}
const pointInPoly = (x: number, y: number, pts: Array<[number, number]>): boolean => {
  let inside = false
  for (let i = 0, j = pts.length - 1; i < pts.length; j = i++) {
    const [xi, yi] = pts[i], [xj, yj] = pts[j]
    if (yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) inside = !inside
  }
  return inside
}
/** Area inside `bounds` covered by at least one polygon (0.5 m raster, overlaps counted once). */
export function coverageUnionArea (polys: CoveragePoly[], bounds: Rect, step = 0.5): number {
  if (!polys.length || bounds.w <= 0 || bounds.d <= 0) return 0
  let cells = 0
  for (let y = bounds.y + step / 2; y < bounds.y + bounds.d; y += step) {
    for (let x = bounds.x + step / 2; x < bounds.x + bounds.w; x += step) {
      for (const p of polys) if (pointInPoly(x, y, p.points)) { cells++; break }
    }
  }
  return Math.round(cells * step * step * 100) / 100
}

// ---- doors / gates live IN walls -----------------------------------------------------------------------------
export interface DoorDock { rect: Rect; wall: WbWall; horizontal: boolean }
const r2d = (v: number) => Math.round(v * 100) / 100
/** Nearest wall (within `tol` m of the door centre) long enough for the door: the door is turned along the wall,
 *  centred on the wall line and kept inside the wall segment. */
export function dockDoorToWall (r: Rect, walls: WbWall[], tol = 2): DoorDock | null {
  const len = Math.max(r.w, r.d), thick = Math.min(r.w, r.d)
  const cx = r.x + r.w / 2, cy = r.y + r.d / 2
  let best: DoorDock | null = null, bd = tol
  for (const w of walls) {
    const [x1, y1, x2, y2] = w
    if (y1 === y2) {
      const lo = Math.min(x1, x2), hi = Math.max(x1, x2)
      if (hi - lo < len - 1e-6) continue
      const d = Math.hypot(cx - Math.min(Math.max(cx, lo), hi), cy - y1)
      if (d < bd) { bd = d; best = { rect: { x: r2d(Math.min(Math.max(cx - len / 2, lo), hi - len)), y: r2d(y1 - thick / 2), w: len, d: thick }, wall: w, horizontal: true } }
    } else {
      const lo = Math.min(y1, y2), hi = Math.max(y1, y2)
      if (hi - lo < len - 1e-6) continue
      const d = Math.hypot(cx - x1, cy - Math.min(Math.max(cy, lo), hi))
      if (d < bd) { bd = d; best = { rect: { x: r2d(x1 - thick / 2), y: r2d(Math.min(Math.max(cy - len / 2, lo), hi - len)), w: thick, d: len }, wall: w, horizontal: false } }
    }
  }
  return best
}
/** True when a wall of the door's orientation runs through the door over its whole length. */
export function doorOnWall (r: Rect, walls: WbWall[], eps = 0.05): boolean {
  const horizontal = r.w >= r.d, cx = r.x + r.w / 2, cy = r.y + r.d / 2
  return walls.some(([x1, y1, x2, y2]) => horizontal
    ? y1 === y2 && Math.abs(y1 - cy) <= r.d / 2 + eps && Math.min(x1, x2) <= r.x + eps && Math.max(x1, x2) >= r.x + r.w - eps
    : x1 === x2 && Math.abs(x1 - cx) <= r.w / 2 + eps && Math.min(y1, y2) <= r.y + eps && Math.max(y1, y2) >= r.y + r.d - eps)
}
