/**
 * The reference factory — the official deadlock-free M1 build.
 *
 * Round 1 shipped melt and then wedged solid at ~tick 1200: luma, coefficient packs and
 * anchor slabs all overproduce, and with no splitter the surplus had nowhere legal to go,
 * so every machine upstream jammed in turn. This layout fixes that with the two tools the
 * graph actually gives you:
 *
 *   - a splitter feeding TWO quantizers. When the press is full, quantizer A1 jams, its
 *     lane backs up, the splitter skips it, and every luma bar goes to A2 -> the crime
 *     quantizer -> macroblock bricks -> uplink. The demuxer never stalls.
 *   - a splitter on the slurry line with one branch to an uplink, so surplus chroma is
 *     voided instead of backing up into the demuxer.
 *
 * Nothing here is priority routing: splitters just skip blocked outputs, and that alone
 * makes the factory self-balancing. Each consumer takes what it can swallow and the
 * surplus keeps walking.
 *
 *   ore x3 -> demuxer -+-> luma    -> [split] -+-> quantizer A1 -> coeff -> DCT press --+
 *                      |                       +-> quantizer A2 -> coeff -> crime -> bricks
 *                      +-> slurry  -> [split] -+-> subsampler 4:2:2 -> 4:2:0 ----------+
 *                      |                       +-> uplink (void)                        |
 *                      +-> mv-flux -> p-caster -> delta wafers ------------+            |
 *                                                                          v            v
 *                                              GOP assembler A <- slab <- DCT press <---+
 *                                                     |
 *                                                     +-> GOP crate -> mosh -+-> MELT -> uplink
 *                                                                            +-> slab -> assembler B
 *                                                                                (i-only) -> mosh
 */
import type { Command, Dir, GameData } from '../contracts';

const N: Dir = 0, E: Dir = 1, S: Dir = 2, W: Dir = 3;

/**
 * The techs this layout needs before it can be built at all — derived from the commands
 * themselves, so it stays true if either the layout or DATA's tree moves.
 *
 * Contracts v4 gates every id a tech names, and this build uses two of them, so it is a
 * post-research save rather than a cold start. Anything driving it (a test, the dev demo
 * hook) has to open these first or the commands are silently dropped and the factory comes
 * up underpowered — measured: the reactor's uplink runs at 100 draw against 80 gen and the
 * whole sector browns out. See NOTES round 4.
 */
export function referenceFactoryTech(data: GameData): string[] {
  const needs = new Set<string>();
  for (const c of referenceFactory(data)) {
    if (c.kind === 'place') needs.add(c.def);
    else if (c.kind === 'setRecipe' && c.recipe !== null) needs.add(c.recipe);
  }
  return data.tech.filter((t) => t.unlocks.some((u) => needs.has(u))).map((t) => t.id);
}

export function referenceFactory(data: GameData): Command[] {
  // Fail loudly and specifically if DATA moves under us — a missing id here would
  // otherwise surface as a mystery deadlock hours later.
  const machine = (id: string): string => {
    if (!data.machines.some((m) => m.id === id)) {
      throw new Error(`referenceFactory: data/machines.json has no machine "${id}"`);
    }
    return id;
  };
  const recipeId = (id: string): string => {
    if (!data.recipes.some((r) => r.id === id)) {
      throw new Error(`referenceFactory: data/recipes.json has no recipe "${id}"`);
    }
    return id;
  };

  const cmds: Command[] = [];
  let placed = 0;

  /** Places a machine and returns the entity id the sim will assign it. */
  const P = (def: string, x: number, y: number, dir: Dir = N): number => {
    cmds.push({ kind: 'place', def: machine(def), pos: { x, y }, dir });
    return ++placed;
  };
  const b = (x: number, y: number, dir: Dir): void => { P('belt', x, y, dir); };
  const runX = (x0: number, x1: number, y: number, dir: Dir): void => {
    const s = x0 <= x1 ? 1 : -1;
    for (let x = x0; ; x += s) { b(x, y, dir); if (x === x1) break; }
  };
  const runY = (y0: number, y1: number, x: number, dir: Dir): void => {
    const s = y0 <= y1 ? 1 : -1;
    for (let y = y0; ; y += s) { b(x, y, dir); if (y === y1) break; }
  };
  const setRecipe = (entity: number, recipe: string): void => {
    cmds.push({ kind: 'setRecipe', entity, recipe: recipeId(recipe) });
  };

  // ---- power: ASIC base + software-decoder burst + tanks.
  //
  // Round 2 ran four naked software decoders. Once DATA gave them real heat they all
  // scrammed in lockstep and the factory blacked out, which is why melt stopped arriving.
  // The fix is the shape the codex always implied: cool inflexible ASICs hold the floor,
  // one hot decoder bursts on top of them, and the tanks ride out its downtime.
  //
  // Sized against the WORST case, not the average: the quantizers and subsamplers hand
  // back ~30/s of compression bandwidth, but not while they're between crafts. Assume
  // none of it. Four ASICs (80/s) against ~107/s draw leaves a 27/s hole for the ~16.7s
  // the decoder is down = ~450 bandwidth-seconds, and three tanks hold 900. Two ASICs
  // fewer and it survives only while compression happens to be running — measured that,
  // it bottomed out at 2.2 of 600, which is not a margin, it's a coincidence.
  for (let i = 0; i < 4; i++) P('decode-asic', -30 + i * 3, -14);
  P('software-decoder', -18, -14);
  P('buffer-tank', -15, -14);
  P('buffer-tank', -12, -14);
  P('buffer-tank', -9, -14);

  // ---- mining. Three seams keep the demuxer at its 45-tick cadence (it eats 3 ore).
  P('seam-extractor', -30, 0);
  P('seam-extractor', -30, -3);
  P('seam-extractor', -30, 3);
  runY(-2, -1, -28, S);   // north seam joins the trunk
  runY(3, 1, -28, N);     // south seam joins the trunk
  runX(-28, -27, 0, E);
  P('demuxer', -26, 0);

  // ---- mv-flux -> delta wafers. This lane is the whole factory's bottleneck (a GOP
  // crate needs 6 wafers = 12 mv-flux = 12 demux crafts), so nothing here overproduces.
  runY(-1, -3, -25, N);
  P('p-caster', -26, -5);
  runX(-26, -11, -6, E);
  runY(-6, -1, -10, S);

  // ---- luma -> two quantizers behind a splitter. A1 feeds the press; when the press is
  // satisfied A1 jams, the splitter skips it, and everything goes to A2 -> crime.
  runX(-24, -23, 0, E);
  P('lane-splitter', -22, 0);

  runX(-21, -20, 0, E);
  P('quantizer', -19, 0);           // A1: quantize (default)
  runX(-17, -16, 0, E);             // coefficient packs -> press
  runY(-1, -2, -19, N);             // hf dust -> uplink
  P('shipper', -19, -4);

  runY(1, 3, -22, S);
  P('quantizer', -23, 4);           // A2: quantize (default)
  runX(-21, -20, 4, E);             // coefficient packs -> crime
  const crime = P('quantizer', -19, 4);
  setRecipe(crime, 'quantize-crime');
  runY(6, 7, -23, S);               // A2's hf dust -> uplink
  P('shipper', -24, 8);
  b(-19, 6, S);                     // crime's bricks + dust -> uplink (two lanes: crime
  b(-18, 6, S);                     // outruns a single belt when A2 is at full tilt)
  b(-18, 7, W);
  P('shipper', -20, 7);

  // ---- chroma. The splitter's void branch is what keeps surplus slurry from backing up
  // into the demuxer and starving the mv-flux lane.
  runY(2, 9, -26, S);
  P('lane-splitter', -26, 10);
  runY(11, 12, -26, S);             // void branch
  P('shipper', -27, 13);
  runX(-25, -24, 10, E);            // working branch
  P('subsampler', -23, 10);         // 4:2:2 (default)
  runX(-21, -20, 10, E);
  const sub420 = P('subsampler', -19, 10);
  setRecipe(sub420, 'subsample-420');
  b(-17, 10, E);
  runY(10, 3, -16, N);              // chroma 4:2:0 climbs to the press
  b(-16, 2, E);

  // ---- the press: 6 coefficient packs + 1 chroma 4:2:0 -> one anchor slab.
  const press = P('dct-press', -15, 0);
  setRecipe(press, 'press-anchor-slab');
  runX(-12, -11, 0, E);

  // ---- assembly -> mosh -> MELT.
  P('gop-assembler', -10, 0);       // assemble-gop (default)
  runX(-7, -6, 0, E);
  P('mosh-reactor', -5, 0);
  // TWO melt lanes, and the second one is not decoration. The reactor pushes one melt and
  // one slab per tick, so with a single lane melt #2 finds its own belt full, sees the slab
  // lane also ends at an uplink (rank 1, same as its own), and takes it. The splitter down
  // there then locks into perfect alternation with the reactor's push order — slab to the
  // assembler, melt to the uplink, forever — and no slab is ever sold. It survives 20,000
  // ticks looking correct. A second lane means melt never wants the slab lane at all.
  runX(-2, -1, 0, E);
  runX(-2, -1, 1, E);
  P('shipper', 0, 0);

  // Recovered anchor slabs come back out of the reactor, and they go two places: the
  // i-only assembler turns 8 of them into another GOP crate (more melt), and the rest are
  // sold. The splitter is what makes it "and" rather than "or" — half the slabs to each,
  // and if the assembler backs up, all of them go out the door instead of wedging the
  // reactor. Selling them is also the only way the demo ever SHIPS a slab, which is what
  // the uplink fiction upstairs is about.
  b(-5, 3, S);
  P('lane-splitter', -5, 4);
  b(-5, 5, S);
  const iOnly = P('gop-assembler', -6, 6);
  setRecipe(iOnly, 'assemble-gop-i-only');
  runY(6, 3, -3, N);                // its crates rejoin the reactor
  runX(-6, -7, 4, W);               // the overflow: slabs for sale
  P('shipper', -9, 3);

  // ---- THE RESEARCH ANNEX (v5), on the eastern seam.
  //
  // Deliberately a separate works rather than a tap off the melt line. The western
  // floorplan has no room left — every column between the quantizers and the press is
  // spoken for — and threading three ingredients through it would have made the melt
  // regression hostage to the science. DATA's second mdat seam (stream-crust-east,
  // x 8..13, y -4..3) is empty ground, so the annex mines its own ore and shares only
  // the bandwidth bus.
  //
  //   ore -> demuxer -+-> luma -> quantizer -+-> crime  -> BRICKS -+
  //                   |                      +-> ringing -> HALOS -+-> bottler -> SPATIAL
  //                   +-> slurry ----------------------------------+      PACK -> lab
  //                   +-> mv-flux -> uplink (nothing here wants it)
  for (let i = 0; i < 2; i++) P('decode-asic', 16 + i * 3, -7); // the annex pays its own way

  P('seam-extractor', 8, -4);
  P('seam-extractor', 8, -1);
  P('seam-extractor', 8, 2);
  runY(-3, 3, 10, S);               // collector column down the seam's east face
  b(10, 4, E);
  b(11, 4, E);
  P('demuxer', 12, 4);

  runX(14, 15, 4, E);               // luma
  P('quantizer', 16, 4);            // 'quantize' by default -> coefficient packs
  b(16, 3, N);                      // its dust
  P('shipper', 15, 1);

  b(18, 4, E);
  P('lane-splitter', 19, 4);        // coefficient packs fork: bricks one way, halos the other
  b(20, 4, E);
  const annexCrime = P('quantizer', 21, 4);
  setRecipe(annexCrime, 'quantize-crime');
  runY(5, 6, 19, S);
  const annexRing = P('quantizer', 18, 7);
  setRecipe(annexRing, 'skim-ringing');

  runX(23, 24, 4, E);               // crime's dust
  b(24, 5, S);
  P('shipper', 24, 6);
  runX(17, 16, 7, W);               // ringing's dust
  P('shipper', 14, 6);

  const bottler = P('artifact-bottler', 21, 8);
  setRecipe(bottler, 'bottle-spatial-pack');
  runY(6, 7, 21, S);                // crime's bricks drop into the bottler
  b(20, 8, E);                      // ringing's halos come in from the west

  runY(6, 9, 13, S);                // slurry takes the long way round the works
  runX(13, 20, 10, E);
  b(21, 10, N);

  runY(6, 8, 12, S);                // mv-flux has no consumer out here — sell it, or the
  P('shipper', 11, 9);              // demuxer jams on its own byproduct

  runX(23, 24, 8, E);               // the packs, at last
  P('archaeology-lab', 25, 8);

  // Aim the labs at the cheapest thing they can actually pay for, so the demo researches
  // something rather than sitting on a full pack rack.
  const target = data.tech
    .filter((t) => Object.keys(t.cost).length === 1 && (t.cost['spatial-pack'] ?? 0) > 0)
    .sort((a, b) => a.cost['spatial-pack'] - b.cost['spatial-pack'])[0];
  if (target) cmds.push({ kind: 'setResearch', tech: target.id });

  return cmds;
}
