/**
 * LANE-RENDER — belt neighbour analysis. PURE PRESENTATION: sim truth is unchanged,
 * we only decide which piece to draw and which path cargo appears to take.
 *
 * A belt's `dir` is its OUTPUT direction. A neighbour feeds it when that neighbour
 * sits on an adjacent tile and points into us. From the set of feeding sides we pick:
 *
 *   straight — fed from behind (or not fed at all)
 *   cornerA  — fed from local +x  (world dir (d+1)%4)
 *   cornerB  — fed from local -x  (world dir (d+3)%4)
 *   merge    — fed from two or more sides
 *
 * "local" means after the instance yaw is applied, so a cornerA piece is always
 * authored the same way and the yaw carries it to any world orientation.
 */
import type { Dir, MachineDef, SimSnapshot } from '../contracts';
import { DIR_VEC, tileKey } from './coords';

export type BeltShape = 'straight' | 'cornerA' | 'cornerB' | 'merge';
export const BELT_SHAPES: BeltShape[] = ['straight', 'cornerA', 'cornerB', 'merge'];

export interface BeltNode {
  id: number;
  def: string;
  x: number;
  y: number;
  dir: Dir;
  shape: BeltShape;
}

const opposite = (d: Dir): Dir => ((d + 2) % 4) as Dir;

export class BeltTopology {
  /** Bumps whenever the belt layout changes; layers rebuild off this. */
  version = 0;

  private nodes = new Map<number, BeltNode>();
  private byTile = new Map<number, BeltNode>();
  private lastSig = '';

  get(id: number): BeltNode | undefined {
    return this.nodes.get(id);
  }

  nodesOf(defId: string, shape: BeltShape): BeltNode[] {
    const out: BeltNode[] = [];
    for (const n of this.nodes.values()) if (n.def === defId && n.shape === shape) out.push(n);
    return out;
  }

  /**
   * Recompute if the belt layout moved. Returns true when it changed.
   * Cheap signature first: belts are static once placed, so most frames do nothing.
   */
  rebuild(snap: SimSnapshot, defs: Map<string, MachineDef>): boolean {
    let sig = '';
    const belts: BeltNode[] = [];
    for (const e of snap.entities) {
      if (defs.get(e.def)?.kind !== 'belt') continue;
      sig += `${e.id},${e.pos.x},${e.pos.y},${e.dir};`;
      belts.push({ id: e.id, def: e.def, x: e.pos.x, y: e.pos.y, dir: e.dir, shape: 'straight' });
    }
    if (sig === this.lastSig) return false;
    this.lastSig = sig;

    this.nodes.clear();
    this.byTile.clear();
    for (const b of belts) {
      this.nodes.set(b.id, b);
      this.byTile.set(tileKey(b.x, b.y), b);
    }

    for (const b of belts) {
      const feeds: Dir[] = [];
      for (let nd = 0 as Dir; nd < 4; nd = (nd + 1) as Dir) {
        if (nd === b.dir) continue; // that side is our output, not an input
        const v = DIR_VEC[nd];
        const n = this.byTile.get(tileKey(b.x + v.x, b.y + v.y));
        if (n && n.dir === opposite(nd)) feeds.push(nd); // it points back into us
      }
      const sideA = ((b.dir + 1) % 4) as Dir;
      const sideB = ((b.dir + 3) % 4) as Dir;
      if (feeds.length >= 2) b.shape = 'merge';
      else if (feeds.length === 1 && feeds[0] === sideA) b.shape = 'cornerA';
      else if (feeds.length === 1 && feeds[0] === sideB) b.shape = 'cornerB';
      else b.shape = 'straight'; // fed from behind, by a machine, or not at all
    }

    this.version++;
    return true;
  }
}

// ---------------------------------------------------------------- cargo pathing

/** cos/sin of the instance yaw (-d*90°), without trig in the hot loop. */
const YAW_COS = [1, 0, -1, 0];
const YAW_SIN = [0, -1, 0, 1];

const HALF_PI = Math.PI / 2;
/** Quarter-arc of radius 0.5 — the distance cargo actually travels around a corner. */
export const ARC_LEN = HALF_PI * 0.5;

/**
 * Where a belt item sits, in LOCAL tile space, at progress t (0..1).
 * Straight: back edge -> front edge. Corner: side edge -> front edge along the arc,
 * so cargo enters where the feeding belt actually hands it over instead of teleporting
 * to the back edge and cutting the corner.
 */
export function localPath(shape: BeltShape, t: number, out: { x: number; z: number }): void {
  if (shape === 'cornerA' || shape === 'cornerB') {
    const side = shape === 'cornerA' ? 1 : -1;
    // Arc centred on the corner where the input and output edges meet.
    const a = HALF_PI + side * t * HALF_PI;
    out.x = side * 0.5 + Math.cos(a) * 0.5;
    out.z = -0.5 + Math.sin(a) * 0.5;
    return;
  }
  out.x = 0;
  out.z = 0.5 - t; // +z (back) -> -z (front)
}

/** Rotate a local tile-space point by the belt's yaw and drop it at the tile centre. */
export function toWorld(
  lx: number,
  lz: number,
  x: number,
  y: number,
  dir: Dir,
  out: { x: number; z: number },
): void {
  const c = YAW_COS[dir];
  const s = YAW_SIN[dir];
  out.x = x + 0.5 + (lx * c + lz * s);
  out.z = y + 0.5 + (-lx * s + lz * c);
}
