// Collision groups — which bodies are allowed to touch which. Called groups, not layers, because
// `layer` already means DRAW order in 2D (`node.layer`, `scene2d.layer(n).ySort()`).
//
//   // game/groups.ts
//   export const player  = Physics2D.addGroup()
//   export const bullets = Physics2D.addGroup().ignoreSelf().ignore(player)
//
//   bullet.aspect(Physics2D, { motion: 'dynamic', group: bullets })
//
// Two decisions worth knowing, both deliberate:
//
// SUBTRACTIVE, not enumerative. A group collides with everything EXCEPT what it ignores, so adding a
// new group later cannot silently stop existing pairs from colliding. The enumerative form —
// "collides only with [ground, enemy]" — decays: introduce `crate` a month later and the bullets
// start flying through it, with nothing to notice.
//
// SYMMETRIC. Box2D's filter is an AND of two masks, so declaring the relation on one side only is the
// classic trap ("A allowed B but B didn't allow A" → they silently pass through). Both sides are set
// from one undirected edge here, so naming it once is enough.
//
// Budget: 32 bits, because a category/mask must cross the bridge as a plain JS number and only 2^32 is
// exactly representable there. One bit is the implicit default group, one is reserved for the
// character's shadow body — 30 user groups.

/** Collides with everything. */
export const ALL_GROUPS = 0xffffffff

/** @internal Bit 0 — every body that never names a group. */
export const DEFAULT_BIT = 1 << 0
/** @internal Bit 1 — the character's shadow body: solid shapes never see it, sensors always do. */
export const CHARACTER_BIT = 1 << 1

const MAX_USER_GROUPS = 30
let nextBit = 2
const all: PhysicsGroup2D[] = []

// Bodies whose filter has to be re-pushed when the topology changes. Only bodies that actually named
// a group register, so a project that never touches groups keeps an empty set.
const listeners = new Set<() => void>()
/** @internal */
export const _watchGroupChanges = (fn: () => void): void => { listeners.add(fn) }
/** @internal */
export const _unwatchGroupChanges = (fn: () => void): void => { listeners.delete(fn) }

export class PhysicsGroup2D {
  /** The single category bit this group owns. */
  readonly bit: number
  /** Optional debug label, so an inspector says "bullets" rather than "group #3". */
  readonly label: string

  private _ignored = 0

  /** @internal — use `Physics2D.addGroup()`. */
  constructor(bit: number, label: string) {
    this.bit = bit
    this.label = label
  }

  /** @internal The Box2D pair for a shape in this group. */
  get category(): number { return this.bit }
  /** @internal */
  get mask(): number { return (ALL_GROUPS & ~this._ignored) >>> 0 }

  /**
   * Stop colliding with these groups — symmetric, so naming it on one side is enough. Safe to call
   * after bodies exist: their filters are re-pushed.
   */
  ignore(...groups: PhysicsGroup2D[]): this {
    for (const g of groups) {
      this._ignored |= g.bit
      g._ignored |= this.bit
    }
    for (const fn of listeners) fn()
    return this
  }

  /** Members of this group don't collide with EACH OTHER — bullets, debris, a ragdoll's own bones. */
  ignoreSelf(): this {
    this._ignored |= this.bit
    for (const fn of listeners) fn()
    return this
  }
}

/** @internal The implicit group of every body that never names one. */
export const defaultGroup = new PhysicsGroup2D(DEFAULT_BIT, "default")

/** @internal Register a new group (see `Physics2D.addGroup`). */
export const _addGroup = (label = ""): PhysicsGroup2D => {
  if (nextBit >= 2 + MAX_USER_GROUPS) {
    throw new Error(`Physics2D.addGroup: out of collision groups (${MAX_USER_GROUPS} max — a category has to cross the bridge as a 32-bit number, and two bits are reserved).`)
  }
  const g = new PhysicsGroup2D((1 << nextBit) >>> 0, label || `group${nextBit - 1}`)
  nextBit++
  all.push(g)
  return g
}

/** @internal Sensors must always be visible to the character's shadow body, solids never. */
export const sensorMask = (g: PhysicsGroup2D | undefined): number =>
  (((g ?? defaultGroup).mask | CHARACTER_BIT) & ALL_GROUPS) >>> 0
/** @internal */
export const solidMask = (g: PhysicsGroup2D | undefined): number =>
  (((g ?? defaultGroup).mask & ~CHARACTER_BIT) & ALL_GROUPS) >>> 0
/** @internal */
export const categoryOf = (g: PhysicsGroup2D | undefined): number => (g ?? defaultGroup).category
