/**
 * Push3 → Solidity transpiler.
 *
 * Uses symbolic stack simulation (SSA-style). Each stack holds Solidity expression strings.
 * EXEC.IF runs both branches speculatively, then emits an if/else with result variables
 * for any stack positions that differ between branches.
 *
 * Only the subset of instructions used in optimizer_v3.push3 is implemented.
 */

import { Node } from './parser.js';

interface TranspilerState {
  dStack:    string[];              // DYADIC stack (Solidity expressions)
  bStack:    string[];              // BOOLEAN stack
  nameStack: string[];              // NAME stack (unbound identifiers)
  lines:     string[];              // emitted Solidity statements
  bindings:  Map<string, string>;   // identifier → Solidity variable name
  varCounter: number;               // fresh variable counter (shared across branches)
  indent:    number;                // current indentation level
}

function freshVar(state: TranspilerState, prefix = 'v'): string {
  return `${prefix}${state.varCounter++}`;
}

function emit(state: TranspilerState, line: string): void {
  const pad = '    '.repeat(state.indent);
  state.lines.push(pad + line);
}

function dpop(state: TranspilerState, ctx: string): string {
  const v = state.dStack.pop();
  if (v === undefined) throw new Error(`DYADIC stack underflow at ${ctx}`);
  return v;
}

function bpop(state: TranspilerState, ctx: string): string {
  const v = state.bStack.pop();
  if (v === undefined) throw new Error(`BOOLEAN stack underflow at ${ctx}`);
  return v;
}

function processNode(node: Node, state: TranspilerState): void {
  switch (node.kind) {
    case 'int':
      state.dStack.push(node.value.toString());
      break;

    case 'bool':
      state.bStack.push(node.value ? 'true' : 'false');
      break;

    case 'name':
      // Unbound name goes to the NAME stack (separate from DYADIC stack)
      state.nameStack.push(node.text);
      break;

    case 'instr':
      processInstruction(node.name, state);
      break;

    case 'list':
      for (const item of node.items) {
        processNode(item, state);
      }
      break;
  }
}

function processInstruction(name: string, state: TranspilerState): void {
  switch (name) {
    // ---- DYADIC stack ops ----
    case 'DYADIC.SWAP': {
      const a = dpop(state, 'DYADIC.SWAP');
      const b = dpop(state, 'DYADIC.SWAP');
      state.dStack.push(a);
      state.dStack.push(b);
      break;
    }
    case 'DYADIC.DUP': {
      const a = dpop(state, 'DYADIC.DUP');
      // Materialise complex expressions before duplicating
      const vname = freshVar(state, 'dup');
      emit(state, `uint256 ${vname} = uint256(${a});`);
      state.dStack.push(vname);
      state.dStack.push(vname);
      break;
    }
    case 'DYADIC.POP': {
      dpop(state, 'DYADIC.POP');
      break;
    }

    // ---- DYADIC arithmetic ----
    case 'DYADIC.*': {
      const b = dpop(state, 'DYADIC.*');
      const a = dpop(state, 'DYADIC.*');
      state.dStack.push(`(${a} * ${b})`);
      break;
    }
    case 'DYADIC./': {
      const b = dpop(state, 'DYADIC./');
      const a = dpop(state, 'DYADIC./');
      // Safe division: div-by-zero → 0, matching Push3 no-op semantics.
      state.dStack.push(`(${b} == 0 ? 0 : ${a} / ${b})`);
      break;
    }
    case 'DYADIC.+': {
      const b = dpop(state, 'DYADIC.+');
      const a = dpop(state, 'DYADIC.+');
      state.dStack.push(`(${a} + ${b})`);
      break;
    }
    case 'DYADIC.-': {
      const b = dpop(state, 'DYADIC.-');
      const a = dpop(state, 'DYADIC.-');
      state.dStack.push(`(${a} - ${b})`);
      break;
    }

    // ---- DYADIC comparisons → BOOLEAN ----
    case 'DYADIC.>': {
      const b = dpop(state, 'DYADIC.>');
      const a = dpop(state, 'DYADIC.>');
      state.bStack.push(`(${a} > ${b})`);
      break;
    }
    case 'DYADIC.<': {
      const b = dpop(state, 'DYADIC.<');
      const a = dpop(state, 'DYADIC.<');
      state.bStack.push(`(${a} < ${b})`);
      break;
    }
    case 'DYADIC.>=': {
      const b = dpop(state, 'DYADIC.>=');
      const a = dpop(state, 'DYADIC.>=');
      state.bStack.push(`(${a} >= ${b})`);
      break;
    }
    case 'DYADIC.<=': {
      const b = dpop(state, 'DYADIC.<=');
      const a = dpop(state, 'DYADIC.<=');
      state.bStack.push(`(${a} <= ${b})`);
      break;
    }

    // ---- Name binding ----
    case 'DYADIC.DEFINE': {
      const val  = dpop(state, 'DYADIC.DEFINE (value)');
      const id   = state.nameStack.pop();
      if (id === undefined) throw new Error('DYADIC.DEFINE: NAME stack underflow');
      const varName = id.toLowerCase();
      emit(state, `uint256 ${varName} = uint256(${val});`);
      state.bindings.set(id, varName);
      break;
    }

    // ---- BOOLEAN ----
    case 'BOOLEAN.NOT': {
      const a = bpop(state, 'BOOLEAN.NOT');
      state.bStack.push(`!(${a})`);
      break;
    }
    case 'BOOLEAN.AND': {
      const b = bpop(state, 'BOOLEAN.AND');
      const a = bpop(state, 'BOOLEAN.AND');
      state.bStack.push(`(${a} && ${b})`);
      break;
    }
    case 'BOOLEAN.OR': {
      const b = bpop(state, 'BOOLEAN.OR');
      const a = bpop(state, 'BOOLEAN.OR');
      state.bStack.push(`(${a} || ${b})`);
      break;
    }

    case 'EXEC.IF':
      throw new Error('EXEC.IF must be handled by processItems');

    default:
      throw new Error(`Unsupported instruction: ${name}`);
  }
}

/**
 * Process a list of nodes sequentially, specially handling EXEC.IF by consuming
 * the next two items as true/false branches.
 */
function processItems(items: Node[], state: TranspilerState): void {
  let i = 0;
  while (i < items.length) {
    const item = items[i];

    // Bound identifier → push its Solidity variable to dStack
    if (item.kind === 'name' && state.bindings.has(item.text)) {
      state.dStack.push(state.bindings.get(item.text)!);
      i++;
      continue;
    }

    // EXEC.IF — consume it plus the next two items (branches)
    if (item.kind === 'instr' && item.name === 'EXEC.IF') {
      const trueBranch  = items[i + 1];
      const falseBranch = items[i + 2];
      if (!trueBranch || !falseBranch) throw new Error('EXEC.IF: missing branches');
      i += 3;
      processExecIf(trueBranch, falseBranch, state);
      continue;
    }

    processNode(item, state);
    i++;
  }
}

function makeSubState(parent: TranspilerState, indentOffset = 1): TranspilerState {
  return {
    dStack:    [...parent.dStack],
    bStack:    [...parent.bStack],
    nameStack: [...parent.nameStack],
    lines:     [],
    bindings:  new Map(parent.bindings),
    varCounter: parent.varCounter,
    indent:    parent.indent + indentOffset,
  };
}

/**
 * Emit an if/else block for EXEC.IF.
 *
 * Both branches are simulated speculatively. We compare final stacks elementwise:
 *   - Positions unchanged in both branches → keep as-is in parent
 *   - Positions that differ → emit a result variable, assign in each branch
 *
 * This correctly handles both "new value produced" and "existing value mutated" cases.
 */
function processExecIf(
  trueBranch:  Node,
  falseBranch: Node,
  state: TranspilerState,
): void {
  const cond = bpop(state, 'EXEC.IF condition');

  const dBefore = [...state.dStack];
  const bBefore = [...state.bStack];

  // --- Simulate TRUE branch ---
  const trueState = makeSubState(state);
  processItems(toItems(trueBranch), trueState);
  state.varCounter = trueState.varCounter;

  // --- Simulate FALSE branch ---
  const falseState = makeSubState(state);
  processItems(toItems(falseBranch), falseState);
  state.varCounter = falseState.varCounter;

  // --- Validate branch stack depths match ---
  if (trueState.dStack.length !== falseState.dStack.length) {
    throw new Error(
      `EXEC.IF: DYADIC stack depth mismatch — ` +
      `true branch produces ${trueState.dStack.length} values, ` +
      `false branch produces ${falseState.dStack.length}`
    );
  }
  if (trueState.bStack.length !== falseState.bStack.length) {
    throw new Error(
      `EXEC.IF: BOOLEAN stack depth mismatch — ` +
      `true branch produces ${trueState.bStack.length} values, ` +
      `false branch produces ${falseState.bStack.length}`
    );
  }

  // --- Compare dStacks elementwise ---
  const maxDLen = trueState.dStack.length; // both are equal after validation
  // result var for each position that changed or is new
  const dResultMap = new Map<number, string>(); // position → result var name
  for (let k = 0; k < maxDLen; k++) {
    const tv = trueState.dStack[k];
    const fv = falseState.dStack[k];
    const bv = dBefore[k]; // undefined for positions beyond dBefore
    if (tv !== undefined && fv !== undefined && tv === fv && tv === bv) {
      continue; // identical in both branches and unchanged from before — skip
    }
    const rv = freshVar(state, 'r');
    emit(state, `uint256 ${rv};`);
    dResultMap.set(k, rv);
  }

  // --- Compare bStacks elementwise ---
  const maxBLen = trueState.bStack.length; // both are equal after validation
  const bResultMap = new Map<number, string>();
  for (let k = 0; k < maxBLen; k++) {
    const tv = trueState.bStack[k];
    const fv = falseState.bStack[k];
    const bv = bBefore[k];
    if (tv !== undefined && fv !== undefined && tv === fv && tv === bv) {
      continue;
    }
    const rv = freshVar(state, 'b');
    emit(state, `bool ${rv};`);
    bResultMap.set(k, rv);
  }

  // --- Build branch assignments ---
  const buildAssignments = (
    branchState: TranspilerState,
    dMap: Map<number, string>,
    bMap: Map<number, string>,
    indentLevel: number,
  ): string[] => {
    const pad = '    '.repeat(indentLevel);
    const assignments: string[] = [];
    for (const [k, rv] of dMap) {
      const val = branchState.dStack[k]!;
      assignments.push(`${pad}${rv} = uint256(${val});`);
    }
    for (const [k, rv] of bMap) {
      const val = branchState.bStack[k]!;
      assignments.push(`${pad}${rv} = ${val};`);
    }
    return assignments;
  };

  const trueAssign  = buildAssignments(trueState,  dResultMap, bResultMap, state.indent + 1);
  const falseAssign = buildAssignments(falseState, dResultMap, bResultMap, state.indent + 1);

  // --- Emit if/else ---
  const pad = '    '.repeat(state.indent);
  state.lines.push(`${pad}if (${cond}) {`);
  state.lines.push(...trueState.lines);
  state.lines.push(...trueAssign);

  const falseBody = [...falseState.lines, ...falseAssign];
  if (falseBody.length > 0) {
    state.lines.push(`${pad}} else {`);
    state.lines.push(...falseBody);
  }
  state.lines.push(`${pad}}`);

  // --- Reconstruct parent stacks ---
  const newDStack: string[] = [];
  for (let k = 0; k < maxDLen; k++) {
    const rv = dResultMap.get(k);
    newDStack.push(rv ?? (dBefore[k] ?? trueState.dStack[k]!));
  }
  const newBStack: string[] = [];
  for (let k = 0; k < maxBLen; k++) {
    const rv = bResultMap.get(k);
    newBStack.push(rv ?? (bBefore[k] ?? trueState.bStack[k]!));
  }

  state.dStack = newDStack;
  state.bStack = newBStack;
}

function toItems(node: Node): Node[] {
  return node.kind === 'list' ? node.items : [node];
}

// ---- Public API ----

export interface TranspileResult {
  functionBody:      string[];
  ciVar:             string;
  anchorShareVar:    string;
  anchorWidthVar:    string;
  discoveryDepthVar: string;
}

/**
 * Transpile a Push3 program (top-level list) into Solidity function body lines.
 *
 * Inputs are primed on the DYADIC stack as 8 dyadic rationals (slot 0 on top,
 * slot 7 at bottom).  Each slot is represented as uint256(inputs[i].mantissa);
 * shift support is reserved for future evolution — callers must pass shift=0.
 *
 * The program must leave exactly 4 values on the DYADIC stack at termination:
 *   top (index 3): capitalInefficiency
 *   index 2:       anchorShare
 *   index 1:       anchorWidth
 *   bottom (index 0): discoveryDepth
 */
export function transpile(program: Node): TranspileResult {
  if (program.kind !== 'list') throw new Error('Expected top-level list');

  // Prime DYADIC stack: slot 7 at bottom (index 0), slot 0 at top (index 7).
  const state: TranspilerState = {
    dStack: [
      'uint256(inputs[7].mantissa)',
      'uint256(inputs[6].mantissa)',
      'uint256(inputs[5].mantissa)',
      'uint256(inputs[4].mantissa)',
      'uint256(inputs[3].mantissa)',
      'uint256(inputs[2].mantissa)',
      'uint256(inputs[1].mantissa)',
      'uint256(inputs[0].mantissa)',
    ],
    bStack:    [],
    nameStack: [],
    lines:     [],
    bindings:  new Map(),
    varCounter: 0,
    indent:    2,
  };

  processItems(program.items, state);

  // Pop 4 outputs: top → ci, then anchorShare, anchorWidth, discoveryDepth.
  // If the stack has fewer than 4 values, fall back to bear defaults for the
  // missing positions — matches Push3 no-op semantics (empty stack → 0/default).
  // If the stack has more than 4 values, take the top 4 and discard the rest.
  const ciVar             = state.dStack.pop() ?? '0';
  const anchorShareVar    = state.dStack.pop() ?? '300000000000000000';
  const anchorWidthVar    = state.dStack.pop() ?? '100';
  const discoveryDepthVar = state.dStack.pop() ?? '300000000000000000';

  return { functionBody: state.lines, ciVar, anchorShareVar, anchorWidthVar, discoveryDepthVar };
}
