/**
 * Lego Ladder - Professional Arduino Exporter
 *
 * Generates Arduino/ESP32 code from a ladder logic program.
 * Supports: contacts, coils, timers, counters, gates, branches, math,
 * one-shot, move, scale, compare, and noop elements.
 */

import type {
  Program, Rung, RungElement,
  Contact, Coil, Timer, Counter, LogicGate, Branch,
  MathElement, OneShot, MoveElement, ScaleElement, NoOp, CompareElement
} from './types';

// ─── Helpers ──────────────────────────────────────────────────────

/** Convert an address string to a valid C variable name. */
function sanitize(name: string): string {
  let s = name.replace(/[^a-zA-Z0-9_]/g, '_');
  if (/^[0-9]/.test(s)) s = 'v_' + s;
  return s;
}

/** Read expression for an address — tries input, memory, then output. */
function readExpr(addr: string): string {
  const v = sanitize(addr);
  return `getValue("${addr}")`;
}

// ─── State tracking collected during traversal ────────────────────

interface ExportContext {
  // Pins mapped to their Arduino pin numbers
  pins: Map<string, number>;
  // All input addresses (digital or analog)
  inputAddresses: Set<string>;
  // All output addresses
  outputAddresses: Set<string>;
  // Memory addresses that need variables
  memoryAddresses: Set<string>;
  // Timers needing state variables
  timers: Timer[];
  // Counters needing state variables
  counters: Counter[];
  // Addresses that need edge-detection (previous state tracking)
  edgeAddresses: Set<string>;
  // Latched outputs (SET/RESET/LATCH/UNLATCH need persistent state)
  latchedOutputs: Set<string>;
  // Toggle outputs
  toggleOutputs: Set<string>;
  // Setup lines
  setupLines: string[];
  // Global variable declarations
  globalVars: string[];
  // Loop body lines
  loopLines: string[];
  // Rung-level expressions collected per rung
  rungExpressions: string[][]; // [rungIndex][expressionPart]
  // Rung-level output actions collected per rung
  rungOutputs: string[][]; // [rungIndex][outputLine]
  // Analog input addresses (for analogRead)
  analogInputs: Set<string>;
}

function createContext(): ExportContext {
  return {
    pins: new Map(),
    inputAddresses: new Set(),
    outputAddresses: new Set(),
    memoryAddresses: new Set(),
    timers: [],
    counters: [],
    edgeAddresses: new Set(),
    latchedOutputs: new Set(),
    toggleOutputs: new Set(),
    setupLines: [],
    globalVars: [],
    loopLines: [],
    rungExpressions: [],
    rungOutputs: [],
    analogInputs: new Set(),
  };
}

// ─── Address classification ───────────────────────────────────────

/** Try to parse a pin number from an address like 'I:0.0' or 'Q:0.0'. */
function parsePin(address: string): number | null {
  const m = address.match(/(\d+)\.(\d+)/);
  if (m) return parseInt(m[1]) * 8 + parseInt(m[2]);
  const m2 = address.match(/^(\d+)$/);
  if (m2) return parseInt(m2[1]);
  return null;
}

/** Check if address looks like a digital I/O (I:0.0, Q:0.0, etc.) */
function isDigitalIO(address: string): boolean {
  return /^[IQ]:\d+\.\d+$/.test(address);
}

/** Check if address looks like an analog input (AI:0, A:0, etc.) */
function isAnalogInput(address: string): boolean {
  return /^(AI|A):?\d+$/.test(address);
}

// ─── First pass: collect all addresses and classify them ──────────

function collectAddresses(_ctx: ExportContext, elements: RungElement[]): void {
  for (const el of elements) {
    switch (el.type) {
      case 'contact': {
        const c = el as Contact;
        _ctx.inputAddresses.add(c.address);
        if (c.edgeType) {
          _ctx.edgeAddresses.add(c.address);
        }
        break;
      }
      case 'coil': {
        const c = el as Coil;
        _ctx.outputAddresses.add(c.address);
        if (c.coilType === 'SET' || c.coilType === 'RESET') {
          _ctx.latchedOutputs.add(c.address);
        }
        if (c.coilType === 'LATCH' || c.coilType === 'UNLATCH') {
          _ctx.latchedOutputs.add(c.address);
        }
        if (c.coilType === 'TOGGLE') {
          _ctx.toggleOutputs.add(c.address);
        }
        break;
      }
      case 'timer': {
        const t = el as Timer;
        if (!_ctx.timers.find((x) => x.instanceId === t.instanceId)) {
          _ctx.timers.push(t);
        }
        break;
      }
      case 'counter': {
        const c = el as Counter;
        if (!_ctx.counters.find((x) => x.instanceId === c.instanceId)) {
          _ctx.counters.push(c);
        }
        if (c.resetAddress) {
          _ctx.inputAddresses.add(c.resetAddress);
        }
        break;
      }
      case 'gate': {
        const g = el as LogicGate;
        g.inputs.forEach((inp) => _ctx.inputAddresses.add(inp));
        _ctx.memoryAddresses.add(g.outputAddress);
        break;
      }
      case 'branch':
        collectAddresses(_ctx, (el as Branch).paths.flat());
        break;
      case 'math': {
        const m = el as MathElement;
        _ctx.inputAddresses.add(m.inputA);
        _ctx.inputAddresses.add(m.inputB);
        _ctx.memoryAddresses.add(m.outputAddress);
        break;
      }
      case 'oneshot': {
        const o = el as OneShot;
        _ctx.inputAddresses.add(o.address);
        _ctx.edgeAddresses.add(o.address);
        break;
      }
      case 'move': {
        const m = el as MoveElement;
        _ctx.inputAddresses.add(m.source);
        _ctx.memoryAddresses.add(m.destination);
        break;
      }
      case 'scale': {
        const s = el as ScaleElement;
        _ctx.inputAddresses.add(s.inputAddress);
        _ctx.memoryAddresses.add(s.destination);
        break;
      }
      case 'noop':
        // nothing to collect
        break;
      case 'compare': {
        const c = el as CompareElement;
        _ctx.inputAddresses.add(c.inputA);
        _ctx.inputAddresses.add(c.inputB);
        _ctx.memoryAddresses.add(c.outputAddress);
        break;
      }
    }
  }
}

// ─── Generate expressions for each element type ───────────────────

/** Generate Arduino code expression for a contact element. */
function exportContact(ctx: ExportContext, comp: Contact): string {
  const addr = comp.address;
  const varName = sanitize(addr);

  if (isDigitalIO(addr)) {
    const pin = parsePin(addr);
    if (pin !== null && !ctx.pins.has(addr)) {
      ctx.pins.set(addr, pin);
    }
    const pinRef = ctx.pins.has(addr) ? String(ctx.pins.get(addr)!) : `PIN_${varName}`;
    if (ctx.pins.has(addr)) {
      ctx.setupLines.push(`pinMode(${pinRef}, INPUT_PULLUP);`);
    }

    const digitalRead = `(digitalRead(${pinRef}) == HIGH)`;
    let baseExpr = comp.contactType === 'NO' ? digitalRead : `(!(${digitalRead}))`;

    // Edge detection
    if (comp.edgeType) {
      const prevVar = `prev_${varName}`;
      ctx.globalVars.push(`bool ${prevVar} = false;`);
      if (!ctx.edgeAddresses.has(addr)) {
        ctx.edgeAddresses.add(addr);
      }
      const currVar = `curr_${varName}`;
      let edgeCondition: string;
      switch (comp.edgeType) {
        case 'RISING':
          edgeCondition = `(${currVar} && !${prevVar})`;
          break;
        case 'FALLING':
          edgeCondition = `(!${currVar} && ${prevVar})`;
          break;
        case 'BOTH':
          edgeCondition = `(${currVar} != ${prevVar})`;
          break;
        default:
          edgeCondition = 'true';
      }
      baseExpr = `bool ${currVar} = ${digitalRead}; (${baseExpr} && ${edgeCondition})`;
    }

    return `(${baseExpr})`;
  }

  // Non-digital input — use getValue helper (memory/output/input lookup)
  let baseExpr: string;
  const valExpr = readExpr(addr);
  if (comp.condition) {
    const opMap: Record<string, string> = {
      '==': '==', '!=': '!=', '>': '>', '<': '<', '>=': '>=', '<=': '<=',
    };
    baseExpr = `(${valExpr} ${opMap[comp.condition.op] || '=='} ${comp.condition.value})`;
  } else {
    baseExpr = `(bool)${valExpr}`;
  }
  baseExpr = comp.contactType === 'NO' ? baseExpr : `(!(${baseExpr}))`;

  // Edge detection for non-digital
  if (comp.edgeType) {
    const prevVar = `prev_${varName}`;
    ctx.globalVars.push(`bool ${prevVar} = false;`);
    ctx.edgeAddresses.add(addr);
    const currVar = `curr_${varName}`;
    let edgeCondition: string;
    switch (comp.edgeType) {
      case 'RISING':
        edgeCondition = `(${currVar} && !${prevVar})`;
        break;
      case 'FALLING':
        edgeCondition = `(!${currVar} && ${prevVar})`;
        break;
      case 'BOTH':
        edgeCondition = `(${currVar} != ${prevVar})`;
        break;
      default:
        edgeCondition = 'true';
    }
    baseExpr = `bool ${currVar} = ${baseExpr}; (${baseExpr} && ${edgeCondition})`;
  }

  return `(${baseExpr})`;
}

/** Generate Arduino code for a coil (output) element. */
function exportCoil(ctx: ExportContext, comp: Coil, rungStateVar: string): string[] {
  const lines: string[] = [];
  const addr = comp.address;
  const varName = sanitize(addr);

  if (isDigitalIO(addr)) {
    const pin = parsePin(addr);
    if (pin !== null && !ctx.pins.has(addr)) {
      ctx.pins.set(addr, pin);
    }
    const pinRef = ctx.pins.has(addr) ? String(ctx.pins.get(addr)!) : `PIN_${varName}`;
    if (ctx.pins.has(addr) && !ctx.setupLines.includes(`pinMode(${pinRef}, OUTPUT);`)) {
      ctx.setupLines.push(`pinMode(${pinRef}, OUTPUT);`);
    }
  }

  switch (comp.coilType) {
    case 'OUTPUT':
      if (comp.value !== undefined) {
        lines.push(`if (${rungStateVar}) { ${varName}_val = ${comp.value}; } else { ${varName}_val = 0; }`);
        if (isDigitalIO(addr)) {
          const pinRef = ctx.pins.has(addr) ? String(ctx.pins.get(addr)!) : `PIN_${varName}`;
          lines.push(`digitalWrite(${pinRef}, ${varName}_val != 0 ? HIGH : LOW);`);
        }
      } else {
        if (isDigitalIO(addr)) {
          const pinRef = ctx.pins.has(addr) ? String(ctx.pins.get(addr)!) : `PIN_${varName}`;
          lines.push(`digitalWrite(${pinRef}, ${rungStateVar} ? HIGH : LOW);`);
        } else {
          lines.push(`setOutput("${addr}", ${rungStateVar});`);
        }
      }
      break;

    case 'SET':
      // When rung is true, set output to true; otherwise keep current value
      if (isDigitalIO(addr)) {
        const pinRef = ctx.pins.has(addr) ? String(ctx.pins.get(addr)!) : `PIN_${varName}`;
        ctx.globalVars.push(`bool ${varName}_state = false;`);
        lines.push(`if (${rungStateVar}) { ${varName}_state = true; }`);
        lines.push(`digitalWrite(${pinRef}, ${varName}_state ? HIGH : LOW);`);
      } else {
        lines.push(`if (${rungStateVar}) { setOutput("${addr}", true); }`);
      }
      break;

    case 'RESET':
      // When rung is true, set output to false; otherwise keep current value
      if (isDigitalIO(addr)) {
        const pinRef = ctx.pins.has(addr) ? String(ctx.pins.get(addr)!) : `PIN_${varName}`;
        ctx.globalVars.push(`bool ${varName}_state = false;`);
        lines.push(`if (${rungStateVar}) { ${varName}_state = false; }`);
        lines.push(`digitalWrite(${pinRef}, ${varName}_state ? HIGH : LOW);`);
      } else {
        lines.push(`if (${rungStateVar}) { setOutput("${addr}", false); }`);
      }
      break;

    case 'TOGGLE':
      // Toggle output state only when rung is true
      if (isDigitalIO(addr)) {
        const pinRef = ctx.pins.has(addr) ? String(ctx.pins.get(addr)!) : `PIN_${varName}`;
        ctx.globalVars.push(`bool ${varName}_state = false;`);
        lines.push(`if (${rungStateVar}) { ${varName}_state = !${varName}_state; }`);
        lines.push(`digitalWrite(${pinRef}, ${varName}_state ? HIGH : LOW);`);
      } else {
        lines.push(`if (${rungStateVar}) { setOutput("${addr}", !getOutputValue("${addr}")); }`);
      }
      break;

    case 'LATCH':
      // Once energized, stays on until UNLATCH clears it
      if (isDigitalIO(addr)) {
        const pinRef = ctx.pins.has(addr) ? String(ctx.pins.get(addr)!) : `PIN_${varName}`;
        ctx.globalVars.push(`bool ${varName}_state = false;`);
        lines.push(`if (${rungStateVar}) { ${varName}_state = true; }`);
        lines.push(`digitalWrite(${pinRef}, ${varName}_state ? HIGH : LOW);`);
      } else {
        lines.push(`if (${rungStateVar}) { setOutput("${addr}", true); }`);
      }
      break;

    case 'UNLATCH':
      // Clears a latched output
      if (isDigitalIO(addr)) {
        const pinRef = ctx.pins.has(addr) ? String(ctx.pins.get(addr)!) : `PIN_${varName}`;
        ctx.globalVars.push(`bool ${varName}_state = false;`);
        lines.push(`if (${rungStateVar}) { ${varName}_state = false; }`);
        lines.push(`digitalWrite(${pinRef}, ${varName}_state ? HIGH : LOW);`);
      } else {
        lines.push(`if (${rungStateVar}) { setOutput("${addr}", false); }`);
      }
      break;
  }

  return lines;
}

/** Generate Arduino code for a logic gate element. */
function exportGate(ctx: ExportContext, comp: LogicGate): string {
  const inputExprs = comp.inputs.map((addr) => readExpr(addr));
  const outVar = sanitize(comp.outputAddress);

  ctx.globalVars.push(`bool ${outVar} = false;`);
  ctx.memoryAddresses.add(comp.outputAddress);

  let expr: string;
  switch (comp.gateType) {
    case 'AND':
      expr = inputExprs.join(' && ');
      break;
    case 'OR':
      expr = inputExprs.join(' || ');
      break;
    case 'XOR': {
      if (inputExprs.length === 2) {
        expr = `(${inputExprs[0]} ^ ${inputExprs[1]})`;
      } else {
        // For multi-input XOR: count true values, odd = true
        expr = `(countTrue(${inputExprs.length}, ${inputExprs.join(', ')}) % 2 == 1)`;
      }
      break;
    }
    case 'NOT':
      expr = `(!(${inputExprs[0]}))`;
      break;
    case 'NAND':
      expr = `(!(${inputExprs.join(' && ')}))`;
      break;
    case 'NOR':
      expr = `(!(${inputExprs.join(' || ')}))`;
      break;
    default:
      expr = 'false';
  }

  // Generate assignment line (returned as an expression that's evaluated)
  return `(${outVar} = (${expr})), ${outVar}`;
}

/** Generate Arduino code for a timer element. */
function exportTimer(ctx: ExportContext, comp: Timer, rungStateVar: string, lastRungStateVar: string): string {
  const id = sanitize(comp.instanceId);
  const preset = comp.preset;

  // Declare timer state variables
  ctx.globalVars.push(`unsigned long timer_${id}_accumulated = 0;`);
  ctx.globalVars.push(`unsigned long timer_${id}_lastMillis = 0;`);

  let logic: string;
  switch (comp.timerType) {
    case 'TON': {
      // On-Delay: accumulate while input is true, output true when accumulated >= preset
      logic = `// TON Timer: ${comp.instanceId} (preset=${preset}ms)\\n` +
        `if (${rungStateVar}) {\\n` +
        `  timer_${id}_accumulated = min(timer_${id}_accumulated + (unsigned long)(millis() - timer_${id}_lastMillis), ${preset});\\n` +
        `} else {\\n` +
        `  timer_${id}_accumulated = 0;\\n` +
        `}\\n` +
        `timer_${id}_lastMillis = millis();\\n` +
        `timer_${id}_done = (timer_${id}_accumulated >= ${preset})`;
      ctx.globalVars.push(`bool timer_${id}_done = false;`);
      break;
    }
    case 'TOF': {
      // Off-Delay: when input goes false, start accumulating; output true while accumulated < preset
      logic = `// TOF Timer: ${comp.instanceId} (preset=${preset}ms)\\n` +
        `if (${rungStateVar}) {\\n` +
        `  timer_${id}_accumulated = 0;\\n` +
        `} else {\\n` +
        `  timer_${id}_accumulated = min(timer_${id}_accumulated + (unsigned long)(millis() - timer_${id}_lastMillis), ${preset});\\n` +
        `}\\n` +
        `timer_${id}_lastMillis = millis();\\n` +
        `(${rungStateVar} || timer_${id}_accumulated < ${preset})`;
      break;
    }
    case 'TP': {
      // Pulse: set accumulated to preset on rising edge, countdown
      logic = `// TP Timer: ${comp.instanceId} (preset=${preset}ms)\\n` +
        `if (${rungStateVar} && !${lastRungStateVar}) {\\n` +
        `  timer_${id}_accumulated = ${preset};\\n` +
        `  timer_${id}_lastMillis = millis();\\n` +
        `} else if (timer_${id}_accumulated > 0) {\\n` +
        `  timer_${id}_accumulated = max(timer_${id}_accumulated - (unsigned long)(millis() - timer_${id}_lastMillis), 0UL);\\n` +
        `  timer_${id}_lastMillis = millis();\\n` +
        `}\\n` +
        `(timer_${id}_accumulated > 0)`;
      break;
    }
    case 'RTO': {
      // Retain On-Delay: like TON but accumulated persists when input goes false
      logic = `// RTO Timer: ${comp.instanceId} (preset=${preset}ms)\\n` +
        `if (${rungStateVar}) {\\n` +
        `  timer_${id}_accumulated = min(timer_${id}_accumulated + (unsigned long)(millis() - timer_${id}_lastMillis), ${preset});\\n` +
        `  timer_${id}_lastMillis = millis();\\n` +
        `}\\n` +
        `(timer_${id}_accumulated >= ${preset})`;
      break;
    }
  }

  return logic;
}

/** Generate Arduino code for a counter element. */
function exportCounter(ctx: ExportContext, comp: Counter, rungStateVar: string, lastRungStateVar: string): string {
  const id = sanitize(comp.instanceId);
  const preset = comp.preset;

  ctx.globalVars.push(`int counter_${id}_current = 0;`);
  ctx.globalVars.push(`bool counter_${id}_lastRung = false;`);

  let resetCheck = '';
  if (comp.resetAddress) {
    resetCheck = `if (${readExpr(comp.resetAddress)}) { counter_${id}_current = 0; counter_${id}_lastRung = ${rungStateVar}; return false; }\\n`;
  }

  let incrementLogic: string;
  let compareLogic: string;
  switch (comp.counterType) {
    case 'CTU':
      incrementLogic = `if (${rungStateVar} && !counter_${id}_lastRung) { counter_${id}_current++; }`;
      compareLogic = `(counter_${id}_current >= ${preset})`;
      break;
    case 'CTD':
      incrementLogic = `if (${rungStateVar} && !counter_${id}_lastRung) { counter_${id}_current--; }`;
      compareLogic = `(counter_${id}_current <= ${preset})`;
      break;
    case 'CTUD':
      incrementLogic = `if (${rungStateVar} && !counter_${id}_lastRung) { counter_${id}_current++; }`;
      compareLogic = `(counter_${id}_current >= ${preset})`;
      break;
  }

  return `// Counter: ${comp.instanceId} (${comp.counterType}, preset=${preset})\\n` +
    `${resetCheck}` +
    `${incrementLogic};\\n` +
    `counter_${id}_lastRung = ${rungStateVar};\\n` +
    `${compareLogic}`;
}

/** Generate Arduino code for a branch element. */
function exportBranch(ctx: ExportContext, comp: Branch, lastRungStateVar: string): string {
  const id = sanitize(comp.id);
  const results: string[] = [];

  for (let i = 0; i < comp.paths.length; i++) {
    const pathExprs: string[] = [];
    for (const el of comp.paths[i]) {
      const expr = exportElementExpr(ctx, el, `rung_${id}_path${i}_state`, lastRungStateVar);
      if (expr) {
        pathExprs.push(expr);
      }
    }
    const combined = pathExprs.length > 0 ? pathExprs.join(' && ') : 'true';
    results.push(`(${combined})`);
  }

  if (comp.logic === 'AND') {
    return `(${results.join(' && ')})`;
  } else {
    return `(${results.join(' || ')})`;
  }
}

/** Generate Arduino code for a math element. */
function exportMath(ctx: ExportContext, comp: MathElement): string {
  const outVar = sanitize(comp.outputAddress);
  ctx.globalVars.push(`float math_${outVar} = 0.0;`);
  ctx.memoryAddresses.add(comp.outputAddress);

  const valA = readExpr(comp.inputA);
  const valB = readExpr(comp.inputB);

  let op: string;
  switch (comp.operator) {
    case 'ADD': op = '+'; break;
    case 'SUB': op = '-'; break;
    case 'MUL': op = '*'; break;
    case 'DIV':
      op = '/';
      break;
  }

  let expr = `(math_${outVar} = (${valA} ${op} ${valB}))`;
  if (comp.operator === 'DIV') {
    expr = `(bool)${valB} ? (${expr}) : 0`;
  }

  return expr;
}

/** Generate Arduino code for a one-shot element. */
function exportOneShot(ctx: ExportContext, comp: OneShot): string {
  const addr = comp.address;
  const varName = sanitize(addr);
  ctx.globalVars.push(`bool prev_${varName} = false;`);
  ctx.inputAddresses.add(addr);
  ctx.edgeAddresses.add(addr);

  const valExpr = readExpr(addr);
  let edgeCheck: string;
  if (comp.edgeType === 'RISING') {
    edgeCheck = `(curr_${varName} && !prev_${varName})`;
  } else {
    edgeCheck = `(!curr_${varName} && prev_${varName})`;
  }

  return `(bool curr_${varName} = ${valExpr}; ${edgeCheck})`;
}

/** Generate Arduino code for a move element. */
function exportMove(ctx: ExportContext, comp: MoveElement): string {
  const destVar = sanitize(comp.destination);
  ctx.globalVars.push(`float move_${destVar} = 0.0;`);
  ctx.memoryAddresses.add(comp.destination);

  return `(move_${destVar} = ${readExpr(comp.source)})`;
}

/** Generate Arduino code for a scale element. */
function exportScale(ctx: ExportContext, comp: ScaleElement): string {
  const destVar = sanitize(comp.destination);
  ctx.globalVars.push(`float scale_${destVar} = 0.0;`);
  ctx.memoryAddresses.add(comp.destination);

  const inputExpr = readExpr(comp.inputAddress);
  const inMin = comp.inMin;
  const inMax = comp.inMax;
  const outMin = comp.outMin;
  const outMax = comp.outMax;
  const range = (inMax - inMin);
  const outRange = (outMax - outMin);

  let scaleExpr: string;
  if (range === 0) {
    scaleExpr = `${outMin}`;
  } else {
    scaleExpr = `(${outMin} + (((${inputExpr} - ${inMin}) / ${range}) * ${outRange}))`;
  }
  // Clamp to output range
  scaleExpr = `constrain(${scaleExpr}, ${outMin}, ${outMax})`;

  return `(scale_${destVar} = ${scaleExpr})`;
}

/** Generate Arduino code for a compare element. */
function exportCompare(ctx: ExportContext, comp: CompareElement): string {
  const outVar = sanitize(comp.outputAddress);
  ctx.globalVars.push(`bool cmp_${outVar} = false;`);
  ctx.memoryAddresses.add(comp.outputAddress);

  const valA = readExpr(comp.inputA);
  const valB = readExpr(comp.inputB);

  const opMap: Record<string, string> = {
    '==': '==', '!=': '!=', '>': '>', '<': '<', '>=': '>=', '<=': '<=',
  };
  const op = opMap[comp.op] || '==';

  return `(cmp_${outVar} = (${valA} ${op} ${valB})), cmp_${outVar}`;
}

/** Generate Arduino code for a noop element. */
function exportNoOp(ctx: ExportContext, comp: NoOp): string {
  const comment = comp.comment ? comp.comment : 'No operation';
  return `/* ${comment} */ true`;
}

// ─── Main element dispatcher ──────────────────────────────────────

function exportElementExpr(ctx: ExportContext, el: RungElement, rungStateVar: string, lastRungStateVar: string): string {
  switch (el.type) {
    case 'contact':
      return exportContact(ctx, el as Contact);
    case 'coil':
      // Coils produce output actions, not boolean expressions
      const outputs = exportCoil(ctx, el as Coil, rungStateVar);
      outputs.forEach((line) => ctx.rungOutputs[ctx.rungOutputs.length - 1].push(line));
      return '';
    case 'gate':
      return exportGate(ctx, el as LogicGate);
    case 'timer':
      return exportTimer(ctx, el as Timer, rungStateVar, lastRungStateVar);
    case 'counter':
      return exportCounter(ctx, el as Counter, rungStateVar, lastRungStateVar);
    case 'branch':
      return exportBranch(ctx, el as Branch, lastRungStateVar);
    case 'math':
      return exportMath(ctx, el as MathElement);
    case 'oneshot':
      return exportOneShot(ctx, el as OneShot);
    case 'move':
      return exportMove(ctx, el as MoveElement);
    case 'scale':
      return exportScale(ctx, el as ScaleElement);
    case 'noop':
      return exportNoOp(ctx, el as NoOp);
    case 'compare':
      return exportCompare(ctx, el as CompareElement);
    default:
      return 'true';
  }
}

// ─── Main export function ─────────────────────────────────────────

export function generateArduinoCode(program: Program): string {
  const ctx = createContext();

  // First pass: collect all addresses from all rungs
  for (const rung of program.rungs) {
    if (!rung.enabled) continue;
    collectAddresses(ctx, rung.series);
  }

  // Classify inputs as digital vs analog
  for (const addr of Array.from(ctx.inputAddresses)) {
    if (isAnalogInput(addr)) {
      ctx.analogInputs.add(addr);
    }
  }

  // Generate global variable declarations
  const globalDecl: string[] = [];

  // Input pin definitions
  const pinDefs: string[] = [];
  ctx.pins.forEach((pin, addr) => {
    pinDefs.push(`#define PIN_${sanitize(addr)} ${pin}`);
  });

  // Analog pin definitions
  for (const addr of Array.from(ctx.analogInputs)) {
    const pin = parsePin(addr);
    if (pin !== null) {
      pinDefs.push(`#define PIN_${sanitize(addr)} ${pin}`);
      ctx.pins.set(addr, pin);
    }
  }

  // Declare state variables for latched outputs
  for (const addr of Array.from(ctx.latchedOutputs)) {
    const v = sanitize(addr);
    if (isDigitalIO(addr)) {
      globalDecl.push(`bool ${v}_state = false;  // Latched output state`);
    }
  }

  // Declare state variables for toggle outputs
  for (const addr of Array.from(ctx.toggleOutputs)) {
    const v = sanitize(addr);
    if (isDigitalIO(addr)) {
      globalDecl.push(`bool ${v}_state = false;  // Toggle output state`);
    }
  }

  // Edge detection previous-state variables
  for (const addr of Array.from(ctx.edgeAddresses)) {
    const v = sanitize(addr);
    globalDecl.push(`bool prev_${v} = false;  // Previous state for edge detection`);
  }

  // Collect all timer/counter/gate/math/scale/compare/move state vars
  // (these are added during second pass, so collect after)

  // Generate setup code
  const setupCode: string[] = [];

  // Serial debug
  setupCode.push(`Serial.begin(115200);`);
  setupCode.push(`Serial.println("Lego Ladder PLC started");`);

  // Pin modes for digital outputs
  const outputPins = new Set<string>();
  ctx.pins.forEach((pin, addr) => {
    if (ctx.outputAddresses.has(addr) && !isAnalogInput(addr)) {
      if (!outputPins.has(String(pin))) {
        outputPins.add(String(pin));
        setupCode.push(`pinMode(${pin}, OUTPUT);`);
      }
    }
  });

  // Pin modes for inputs
  const inputPins = new Set<string>();
  ctx.pins.forEach((pin, addr) => {
    if (ctx.inputAddresses.has(addr) && !isAnalogInput(addr) && isDigitalIO(addr)) {
      if (!inputPins.has(String(pin))) {
        inputPins.add(String(pin));
        setupCode.push(`pinMode(${pin}, INPUT_PULLUP);`);
      }
    }
  });

  // Generate rung evaluation code (second pass)
  const loopBody: string[] = [];
  const delayMs = program.cycleTime || 100;

  for (let idx = 0; idx < program.rungs.length; idx++) {
    const rung = program.rungs[idx];
    if (!rung.enabled) continue;

    const rungLabel = rung.label ? rung.label : `Rung${idx}`;
    const rungStateVar = `rung${idx}_state`;
    const lastRungStateVar = `rung${idx}_lastState`;

    // Declare last-state tracking variable
    globalDecl.push(`bool ${lastRungStateVar} = false;`);

    loopBody.push(`// ─── ${rungLabel} ───`);

    // Evaluate series elements
    const exprParts: string[] = [];

    for (const el of rung.series) {
      const expr = exportElementExpr(ctx, el, rungStateVar, lastRungStateVar);
      if (expr && el.type !== 'coil') {
        exprParts.push(expr);
      }
    }

    // Combine expression parts with AND logic
    const combined = exprParts.filter(e => e.trim()).join(' && ');
    const finalExpr = combined || 'false';

    loopBody.push(`bool ${rungStateVar} = ${finalExpr};`);

    // Output actions for this rung (coils, etc.)
    if (ctx.rungOutputs.length > 0 && ctx.rungOutputs[ctx.rungOutputs.length - 1].length > 0) {
      ctx.rungOutputs[ctx.rungOutputs.length - 1].forEach((line) => {
        loopBody.push(line);
      });
    }

    // Update last state for edge detection
    loopBody.push(`${lastRungStateVar} = ${rungStateVar};`);
    loopBody.push('');
  }

  // Edge detection update (update previous input states at end of scan)
  const edgeUpdateLines: string[] = [];
  for (const addr of Array.from(ctx.edgeAddresses)) {
    const v = sanitize(addr);
    if (isDigitalIO(addr)) {
      const pinRef = ctx.pins.has(addr) ? String(ctx.pins.get(addr)!) : `PIN_${v}`;
      edgeUpdateLines.push(`prev_${v} = (digitalRead(${pinRef}) == HIGH);`);
    } else {
      edgeUpdateLines.push(`prev_${v} = (bool)getValue("${addr}");`);
    }
  }
  if (edgeUpdateLines.length > 0) {
    loopBody.push('// ─── Update edge detection state ───');
    edgeUpdateLines.forEach((l) => loopBody.push(l));
  }

  // Scan cycle delay
  loopBody.push(`delay(${delayMs});`);

  // Collect global variable declarations (gathered during traversal)
  const allGlobalVars = deduplicate([...globalDecl, ...ctx.globalVars]);

  // Build final Arduino sketch
  const lines: string[] = [];
  lines.push('// ═══════════════════════════════════════════════════════════');
  lines.push(`// Generated by Lego Ladder - Arduino/ESP32 PLC Exporter`);
  lines.push(`// Program: ${program.name}`);
  lines.push(`// Rungs: ${program.rungs.length}  |  Cycle time: ${delayMs}ms`);
  lines.push('// ═══════════════════════════════════════════════════════════');
  lines.push('');

  if (pinDefs.length > 0) {
    lines.push('// ─── Pin Definitions ───');
    lines.push(...pinDefs);
    lines.push('');
  }

  if (allGlobalVars.length > 0) {
    lines.push('// ─── Global State Variables ───');
    lines.push(...allGlobalVars);
    lines.push('');
  }

  // Helper function for value lookups
  lines.push('// ─── Helper Functions ───');
  lines.push('// Generic value reader: tries inputs, memory, then outputs');
  lines.push('// Note: for performance on small Arduino boards, replace with');
  lines.push('// direct variable references in the generated code.');
  lines.push('');
  lines.push('float getValue(const char* addr) {');
  lines.push('  // This is a placeholder. In a real deployment, each address');
  lines.push('  // maps to a specific variable or pin read.');
  lines.push('  return 0.0;');
  lines.push('}');
  lines.push('');
  lines.push('float getOutputValue(const char* addr) {');
  lines.push('  return 0.0;');
  lines.push('}');
  lines.push('');
  lines.push('void setOutput(const char* addr, bool val) {');
  lines.push('  (void)addr; (void)val; // Placeholder');
  lines.push('}');
  lines.push('');
  lines.push('// Multi-input XOR helper: count true values, return count');
  lines.push('int countTrue(int n, ...) {');
  lines.push('  (void)n; (void)n; // Simplified - in practice use direct logic');
  lines.push('  return 0;');
  lines.push('}');
  lines.push('');

  lines.push('void setup() {');
  setupCode.forEach((l) => lines.push(`  ${l}`));
  lines.push('}');
  lines.push('');
  lines.push('void loop() {');
  loopBody.forEach((l) => lines.push(`  ${l}`));
  lines.push('}');

  return lines.join('\n');
}

/** Remove duplicate lines preserving order. */
function deduplicate(arr: string[]): string[] {
  const seen = new Set<string>();
  return arr.filter((item) => {
    if (seen.has(item)) return false;
    seen.add(item);
    return true;
  });
}