/**
 * LANE-UI — the player's current intent: a pending build, remove mode, or an inspected
 * unit. All three are published through `bus.setSelection()` (contracts v4/v5), and they
 * are mutually exclusive — you hold the wrecking ball, or a machine, or you're looking at
 * a unit, never two at once.
 *
 * main.ts owns the state and decides what a world click means (it places/removes); the
 * renderer reads the selection to draw the ghost, the demolition tint, or (v5) the
 * inspect highlight. We only say what is held.
 *
 * `selection`/`setSelection` are optional in the contract, so we fall back to the legacy
 * `_sel` field if a host doesn't serve them yet (live.test.ts's own bus, for one). The
 * legacy field can't express inspect mode, which is fine — it's a v5 host that highlights.
 */
import type { Dir, SelectionState, UIBus } from '../contracts';

export interface BuildSelection {
  /** The pending *build*, or null. Null whenever removing or inspecting. */
  get(): { def: string; dir: Dir } | null;
  /** Select a machine; selecting the one already selected clears it (toggle). */
  select(def: string): void;
  rotate(): void;
  /** Drop everything — build, wrecking ball, and inspect. */
  clear(): void;
  isRemoving(): boolean;
  toggleRemove(): void;
  /** v5: publish that the inspector is showing this entity, so the renderer highlights it. */
  inspect(entityId: number): void;
  /** v5: the inspector closed — stop highlighting (no-op if we weren't inspecting). */
  clearInspect(): void;
  isInspecting(): boolean;
  /** True when the player is holding a tool (build or wrecking ball). Inspect is not a tool. */
  isArmed(): boolean;
  onChange(cb: () => void): void;
}

type LegacyBus = UIBus & { _sel?: { def: string; dir: Dir } | null };

export function createSelection(bus: UIBus): BuildSelection {
  let sel: { def: string; dir: Dir } | null = null;
  let removing = false;
  let inspecting: number | null = null;
  const listeners: Array<() => void> = [];

  function publish() {
    const state: SelectionState = removing
      ? { mode: 'remove' }
      : sel
        ? { mode: 'build', def: sel.def, dir: sel.dir }
        : inspecting !== null
          ? { mode: 'inspect', entity: inspecting }
          : null;

    if (bus.setSelection) {
      bus.setSelection(state);
    } else {
      // Legacy host: keep the old build/remove shape alive. Inspect has no legacy form —
      // a host without setSelection simply won't get the highlight, which is acceptable.
      (bus as LegacyBus)._sel = removing ? { def: '__remove', dir: 0 } : sel;
    }
  }

  function commit(next: {
    sel?: { def: string; dir: Dir } | null;
    removing?: boolean;
    inspecting?: number | null;
  }) {
    // Any explicit field wins; unspecified tool fields reset, because the three modes are
    // mutually exclusive and a partial update would leave two of them live.
    sel = next.sel !== undefined ? next.sel : null;
    removing = next.removing ?? false;
    inspecting = next.inspecting !== undefined ? next.inspecting : null;
    publish();
    for (const cb of listeners) cb();
  }

  return {
    get: () => (removing || inspecting !== null ? null : sel),
    select(def) {
      // Picking a machine puts the wrecking ball down and stops inspecting.
      if (!removing && sel?.def === def) commit({}); // toggle off
      else commit({ sel: { def, dir: sel?.dir ?? 0 } }); // keep the dir across swaps
    },
    rotate() {
      if (!removing && sel) commit({ sel: { def: sel.def, dir: ((sel.dir + 1) % 4) as Dir } });
    },
    clear() {
      if (sel || removing || inspecting !== null) commit({});
    },
    isRemoving: () => removing,
    toggleRemove() {
      commit({ removing: !removing });
    },
    inspect(entityId) {
      if (inspecting === entityId && !sel && !removing) return; // already showing it
      commit({ inspecting: entityId });
    },
    clearInspect() {
      if (inspecting !== null) commit({});
    },
    isInspecting: () => inspecting !== null,
    isArmed: () => removing || sel !== null,
    onChange(cb) {
      listeners.push(cb);
    },
  };
}
