/**
 * Ladder Logic Engine
 * Evaluates rungs in a scan cycle:
 * 1. Read all inputs
 * 2. Evaluate rungs top-to-bottom, left-to-right
 * 3. Update all outputs
 */

import type {
  Contact,
  Coil,
  Timer,
  Counter,
  LogicGate,
  Rung,
  RungElement,
  ComparisonCondition,
  Program,
  EngineState,
  Input,
  Output,
  MemoryBit,
  ContactType,
  CoilType,
  TimerType,
  CounterType,
  GateType,
  ComparisonOp,
  Branch,
  MathElement,
  MathOperator,
  OneShot,
  MoveElement,
  ScaleElement,
  NoOp,
  CompareElement,
  EdgeType,
} from './types';
import {
  LadderError,
  InvalidMemoryAccessError,
  MathOperationError,
} from './errors';

export class LadderEngine {
  state: EngineState;
  private program: Program | null = null;
  private listeners: Set<() => void> = new Set();
  private lastRungStates: Map<string, boolean> = new Map();
  private stopFn: (() => void) | null = null;

  constructor() {
    this.state = {
      cycle: 0,
      inputs: new Map(),
      outputs: new Map(),
      memory: new Map(),
      timers: new Map(),
      counters: new Map(),
      previousInputs: new Map(),
      running: false,
    };
  }

  /** Subscribe to state changes. */
  subscribe(listener: () => void): () => void {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  }

  private notify() {
    this.listeners.forEach((l) => l());
  }

  /** Initialize or replace the program. */
  loadProgram(program: Program): void {
    this.stop();
    this.program = program;
    this.state.outputs.clear();
    this.state.inputs.clear();
    this.state.memory.clear();
    this.state.timers.clear();
    this.state.counters.clear();
    this.lastRungStates.clear();
    this.notify();
  }

  /** Get current engine state. */
  getState(): Readonly<EngineState> {
    return this.state;
  }

  /** Get the loaded program. */
  getProgram(): Program | null {
    return this.program;
  }

  // ─── Mutations ──────────────────────────────────────────────────

  /** Update a component's parameters within a rung. */
  updateComponent(rungId: string, componentId: string, updates: Partial<any>): void {
    if (!this.program) return;
    const rung = this.program.rungs.find((r) => r.id === rungId);
    if (!rung) return;

    const found = this.findComponentRecursive(rung.series, componentId);
    if (found) {
      Object.assign(found.element, updates);
      this.notify();
    }
  }

  /** Move a component from one rung to another or within the same rung. */
  moveComponent(
    sourceRungId: string,
    componentId: string,
    targetRungId: string,
    targetPosition: number,
  ): void {
    if (!this.program) return;
    const sourceRung = this.program.rungs.find((r) => r.id === sourceRungId);
    const targetRung = this.program.rungs.find((r) => r.id === targetRungId);
    if (!sourceRung || !targetRung) return;

    const found = this.findComponentRecursive(sourceRung.series, componentId);
    if (!found) return;

    const { parent, index, element } = found;
    parent.splice(index, 1);

    // Insert into target rung's series
    (element as any).position = targetPosition;
    targetRung.series.splice(targetPosition, 0, element);

    // Re-index positions
    this.reindexSeries(sourceRung.series);
    this.reindexSeries(targetRung.series);

    this.notify();
  }

  /** Toggle an input state. */
  toggleInput(address: string): void {
    const current = this.state.inputs.get(address)?.value ?? false;
    this.setInput(address, !current);
  }

  // ─── I/O Operations ────────────────────────────────────────────

  /** Set an input value. */
  setInput(address: string, value: boolean | number, rawValue?: number): void {
    const existing = this.state.inputs.get(address);
    this.state.inputs.set(address, {
      address,
      value,
      rawValue: rawValue ?? existing?.rawValue,
    });
    this.notify();
  }

  /** Read an input value. */
  getInput(address: string): boolean | number {
    return this.state.inputs.get(address)?.value ?? false;
  }

  /** Read an output value. */
  getOutput(address: string): boolean | number {
    return this.state.outputs.get(address)?.value ?? false;
  }

  /** Get all outputs. */
  getOutputs(): Map<string, Output> {
    return this.state.outputs;
  }

  /** Set a memory bit directly. */
  setMemory(address: string, value: boolean | number): void {
    const existing = this.state.memory.get(address);
    if (existing) {
      existing.value = value;
    } else {
      this.state.memory.set(address, { address, value });
    }
    this.notify();
  }

  /** Read a memory bit. */
  getMemory(address: string): boolean | number {
    return this.state.memory.get(address)?.value ?? false;
  }

  // ─── Scan Cycle ────────────────────────────────────────────────

  /** Run one scan cycle of the ladder program. */
  scan(): void {
    if (!this.program) return;

    this.state.cycle++;

    // Save current inputs as previous for edge detection (all address spaces)
    const currentInputs = new Map<string, boolean | number>();
    this.state.inputs.forEach((input, addr) => {
      currentInputs.set(addr, input.value);
    });
    this.state.memory.forEach((mem, addr) => {
      currentInputs.set(addr, mem.value);
    });
    this.state.outputs.forEach((out, addr) => {
      currentInputs.set(addr, out.value);
    });

    // Handle resets
    this.handleCounterResets();
    this.handleTimerResets(this.program);

    // Evaluate each rung
    for (const rung of this.program.rungs) {
      try {
        const lastRungState = this.lastRungStates.get(rung.id) ?? false;
        if (!rung.enabled) {
          this.lastRungStates.set(rung.id, false);
          continue;
        }
        const currentRungState = this.evaluateRung(rung, lastRungState);
        this.lastRungStates.set(rung.id, currentRungState);
      } catch (err) {
        console.error(`Error in scan cycle for rung ${rung.id}:`, err);
        // We continue to the next rung even if one fails
      }
    }

    // Update previousInputs for next scan cycle
    this.state.previousInputs = currentInputs;

    this.notify();
  }

  /** Start the engine running. */
  start(): () => void {
    if (!this.program) return () => {};
    if (this.stopFn) this.stop();

    this.state.running = true;
    this.notify();
    const interval = this.program.cycleTime;
    const timer = setInterval(() => this.scan(), interval);

    this.stopFn = () => {
      this.state.running = false;
      this.notify();
      clearInterval(timer);
    };

    return this.stopFn;
  }

  /** Stop the engine running. */
  stop(): void {
    if (this.stopFn) {
      this.stopFn();
      this.stopFn = null;
    }
  }

  private handleCounterResets(): void {
    if (!this.program) return;
    for (const rung of this.program.rungs) {
      this.traverseRung(rung, (element) => {
        if (element.type === 'counter') {
          const counter = element as Counter;
          if (counter.resetAddress) {
            const resetValue = this.getInput(counter.resetAddress) || this.getMemory(counter.resetAddress) || this.getOutput(counter.resetAddress);
            if (resetValue) {
              this.state.counters.set(counter.instanceId, { ...counter, current: 0 });
            }
          }
        }
      });
    }
  }

  private handleTimerResets(program: Program): void {
    for (const rung of program.rungs) {
      if (!rung.enabled) continue;
      for (const element of rung.series) {
        if (element.type === 'timer') {
          const timer = element as Timer;
          if (timer.timerType === 'RTO' && timer.resetAddress) {
            const resetState =
              this.getInput(timer.resetAddress) ||
              this.getMemory(timer.resetAddress) ||
              this.getOutput(timer.resetAddress) ||
              false;
            if (resetState) {
              const existing = this.state.timers.get(timer.instanceId);
              if (existing) {
                existing.accumulated = 0;
              } else {
                this.state.timers.set(timer.instanceId, {
                  ...timer,
                  accumulated: 0,
                });
              }
            }
          }
        }
      }
    }
  }

  // ─── Rung Evaluation ───────────────────────────────────────────

  private evaluateRung(rung: Rung, lastRungState: boolean): boolean {
    const rungResult = this.evaluateSeries(rung.series, lastRungState);

    // The last element in series is typically the output coil
    const lastElement = rung.series[rung.series.length - 1];
    if (lastElement && lastElement.type === 'coil') {
      this.activateCoil(lastElement as Coil, rungResult, lastRungState);
    }

    return rungResult;
  }

  private evaluateSeries(elements: RungElement[], lastRungState: boolean): boolean {
    let result = true;
    for (const element of elements) {
      try {
        switch (element.type) {
          case 'contact':
            result = result && this.evaluateContact(element as Contact);
            break;
          case 'gate':
            result = result && this.evaluateGate(element as LogicGate);
            break;
          case 'timer': {
            const timerResult = this.evaluateTimer(element as Timer, result, lastRungState);
            const t = element as Timer;
            if (t.timerType === 'TOF') {
              // TOF controls its own output independently of rung state:
              // while timing (off-delay), it overrides the series result
              result = timerResult;
            } else {
              result = result && timerResult;
            }
            break;
          }
          case 'counter': {
            const counterResult = this.evaluateCounter(element as Counter, result, lastRungState);
            result = result && counterResult;
            break;
          }
          case 'branch':
            result = result && this.evaluateBranch(element as Branch, lastRungState);
            break;
          case 'math': {
            const mathResult = this.evaluateMath(element as MathElement, result);
            result = result && mathResult;
            break;
          }
          case 'oneshot': {
            const oneshotResult = this.evaluateOneShot(element as OneShot, result, lastRungState);
            result = result && oneshotResult;
            break;
          }
          case 'move': {
            const moveResult = this.evaluateMove(element as MoveElement, result);
            result = result && moveResult;
            break;
          }
          case 'scale': {
            const scaleResult = this.evaluateScale(element as ScaleElement, result);
            result = result && scaleResult;
            break;
          }
          case 'noop':
            // NOP is a no-op, passes through as true
            break;
          case 'compare': {
            const compareResult = this.evaluateCompare(element as CompareElement, result);
            result = result && compareResult;
            break;
          }
          case 'coil':
            break;
          default:
            break;
        }
      } catch (err) {
        console.error(`Error evaluating element ${element.type}:`, err);
        // In a real PLC, this might halt the scan or set a fault bit.
        // Here, we'll treat it as a failure of this part of the rung.
        result = false;
      }
    }
    return result;
  }

  private evaluateBranch(branch: Branch, lastRungState: boolean): boolean {
    if (branch.logic === 'AND') {
      return branch.paths.every((path) => this.evaluateSeries(path, lastRungState));
    } else {
      return branch.paths.some((path) => this.evaluateSeries(path, lastRungState));
    }
  }

  private evaluateContact(contact: Contact): boolean {
    let inputValue: boolean | number;

    if (this.state.inputs.has(contact.address)) {
      inputValue = this.getInput(contact.address);
    } else if (this.state.memory.has(contact.address)) {
      inputValue = this.getMemory(contact.address);
    } else if (this.state.outputs.has(contact.address)) {
      inputValue = this.getOutput(contact.address);
    } else {
      // No address found — treat as false input
      inputValue = false;
    }

    // Evaluate the base contact logic (NO/NC with optional condition)
    let baseResult: boolean;
    if (contact.condition) {
      const rawValue =
        this.state.inputs.get(contact.address)?.rawValue ?? inputValue;
      if (typeof rawValue !== 'number') return false;
      baseResult = this.compare(rawValue, contact.condition.op, contact.condition.value);
    } else {
      baseResult = !!inputValue;
    }

    const contactResult = contact.contactType === 'NO' ? baseResult : !baseResult;

    // Edge detection: if edgeType is set, only pass on the specified edge
    if (contact.edgeType) {
      const prevValue = this.state.previousInputs.get(contact.address);
      const currentBool = !!inputValue;
      const prevBool = prevValue !== undefined ? !!prevValue : false;

      if (contact.edgeType === 'RISING') {
        return contactResult && currentBool && !prevBool;
      } else if (contact.edgeType === 'FALLING') {
        return contactResult && !currentBool && prevBool;
      } else if (contact.edgeType === 'BOTH') {
        return contactResult && (currentBool !== prevBool);
      }
    }

    return contactResult;
  }

  private activateCoil(coil: Coil, energized: boolean, lastRungState: boolean = false): void {
    let newValue: boolean | number;
    switch (coil.coilType) {
      case 'OUTPUT':
      case 'OTE':
        // OTE = Output Energize — standard coil that follows rung state
        newValue = energized ? (coil.value ?? true) : (coil.value ?? false);
        break;
      case 'SET':
      case 'OTL':
      case 'LATCH':
        // OTL/LATCH/SET: once energized, stays on until OTU/UNLATCH/RESET clears it
        newValue = energized ? true : (this.state.outputs.get(coil.address)?.value ?? false);
        break;
      case 'RESET':
      case 'OTU':
      case 'UNLATCH':
        // OTU/UNLATCH/RESET: clears a latched output
        newValue = energized ? false : (this.state.outputs.get(coil.address)?.value ?? false);
        break;
      case 'TOGGLE':
        // Only toggle on rising edge (FALSE→TRUE transition), not every scan while held
        if (energized && !lastRungState) {
          const currentToggle = this.state.outputs.get(coil.address)?.value ?? false;
          newValue = !currentToggle;
        } else {
          // Rung held high or false — don't change the output
          return;
        }
        break;
      default:
        newValue = energized;
    }
    this.state.outputs.set(coil.address, {
      address: coil.address,
      value: newValue,
      lastUpdated: this.state.cycle,
    });
  }

  private evaluateTimer(
    timer: Timer,
    rungState: boolean,
    lastRungState: boolean,
  ): boolean {
    const existing = this.state.timers.get(timer.instanceId);
    const currentTimer = existing ?? { ...timer, accumulated: 0 };
    const cycleTime = this.program?.cycleTime ?? 100;

    switch (timer.timerType) {
      case 'TON':
        if (rungState) {
          currentTimer.accumulated = Math.min(
            currentTimer.accumulated + cycleTime,
            timer.preset,
          );
        } else {
          currentTimer.accumulated = 0;
        }
        break;
      case 'TOF':
        if (rungState) {
          // Input is TRUE — reset timer, output ON immediately
          currentTimer.accumulated = 0;
        } else {
          // Input went FALSE — start off-delay timing
          currentTimer.accumulated = Math.min(
            currentTimer.accumulated + cycleTime,
            timer.preset,
          );
        }
        break;
      case 'TP':
        if (rungState && !lastRungState) {
          currentTimer.accumulated = timer.preset;
        } else if (currentTimer.accumulated > 0) {
          currentTimer.accumulated = Math.max(
            currentTimer.accumulated - cycleTime,
            0,
          );
        }
        break;
      case 'RTO':
        // Retain On-Delay: like TON but accumulated time persists when input goes false
        // Needs explicit reset to clear accumulated time
        if (timer.resetAddress) {
          const resetState =
            this.getInput(timer.resetAddress) ||
            this.getMemory(timer.resetAddress) ||
            this.getOutput(timer.resetAddress) ||
            false;
          if (resetState) {
            currentTimer.accumulated = 0;
            break; // Don't accumulate this cycle when reset is asserted
          }
        }
        if (rungState) {
          currentTimer.accumulated = Math.min(
            currentTimer.accumulated + cycleTime,
            timer.preset,
          );
        }
        // When rungState is false, accumulated time is retained (not reset)
        break;
    }

    this.state.timers.set(timer.instanceId, currentTimer);

    if (timer.timerType === 'TON') return currentTimer.accumulated >= timer.preset;
    if (timer.timerType === 'TOF') {
      // TOF: power flows when input is true OR when timer is still timing (not yet done)
      // rungState=true → output ON (accumulated=0, not done)
      // rungState=false → output ON while accumulated < preset, OFF when done
      return rungState || currentTimer.accumulated < timer.preset;
    }
    if (timer.timerType === 'TP') return currentTimer.accumulated > 0;
    if (timer.timerType === 'RTO') return currentTimer.accumulated >= timer.preset;

    return false;
  }

  private evaluateCounter(
    counter: Counter,
    rungState: boolean,
    lastRungState: boolean,
  ): boolean {
    const existing = this.state.counters.get(counter.instanceId);
    const currentCounter = existing ?? { ...counter, current: 0 };

    // Check for reset first. If resetting, we set current to 0 and do not increment.
    if (counter.resetAddress) {
      const resetValue = this.getInput(counter.resetAddress) || this.getMemory(counter.resetAddress) || this.getOutput(counter.resetAddress);
      if (resetValue) {
        currentCounter.current = 0;
        currentCounter.ov = false;
        currentCounter.und = false;
        this.state.counters.set(counter.instanceId, currentCounter);
        return false;
      }
    }

    if (rungState && !lastRungState) {
      if (counter.counterType === 'CTU' || counter.counterType === 'CTUD') {
        currentCounter.current++;
        if (currentCounter.current > 9999) {
          currentCounter.ov = true;
          currentCounter.current = 9999;
        }
      } else if (counter.counterType === 'CTD') {
        currentCounter.current--;
        if (currentCounter.current < 0) {
          currentCounter.und = true;
          currentCounter.current = 0;
        }
      }
    }

    this.state.counters.set(counter.instanceId, currentCounter);

    if (counter.counterType === 'CTU' || counter.counterType === 'CTUD') {
      return currentCounter.current >= counter.preset;
    } else if (counter.counterType === 'CTD') {
      return currentCounter.current <= 0;
    }

    return false;
  }

  private evaluateGate(gate: LogicGate): boolean {
    const inputValues = gate.inputs.map(
      (addr) => {
        if (this.state.inputs.has(addr)) return !!this.getInput(addr);
        if (this.state.memory.has(addr)) return !!this.getMemory(addr);
        if (this.state.outputs.has(addr)) return !!this.getOutput(addr);
        return false;
      },
    );
    switch (gate.gateType) {
      case 'AND': return inputValues.every(Boolean);
      case 'OR': return inputValues.some(Boolean);
      case 'XOR': return inputValues.filter(Boolean).length % 2 === 1;
      case 'NOT': return !inputValues[0];
      case 'NAND': return !inputValues.every(Boolean);
      case 'NOR': return !inputValues.some(Boolean);
      default: return false;
    }
  }

  private evaluateMath(math: MathElement, rungState: boolean): boolean {
    if (!rungState) return false;

    const valA = this.getValueFromAddress(math.inputA);
    const valB = this.getValueFromAddress(math.inputB);

    if (typeof valA !== 'number' || typeof valB !== 'number') {
      throw new MathOperationError(math.operator, 'Inputs must be numbers');
    }

    let result: number;
    switch (math.operator) {
      case 'ADD': result = valA + valB; break;
      case 'SUB': result = valA - valB; break;
      case 'MUL': result = valA * valB; break;
      case 'DIV':
        if (valB === 0) throw new MathOperationError(math.operator, 'Division by zero');
        result = valA / valB;
        break;
      default: throw new MathOperationError(math.operator, 'Unknown operator');
    }

    // Integer mode: truncate to whole number (32-bit DINT style)
    if (this.program?.integerMode) {
      result = Math.trunc(result);
    }
    // 32-bit overflow protection
    result = Math.max(-2147483648, Math.min(2147483647, result));

    // Update the output address. Since it's math, we might want to update an output or memory.
    // For simplicity, we'll assume it's an output or memory.
    // If it's an output, we use the existing logic. 
    // Note: the current activateCoil only handles boolean/number for COILS.
    // We might need a more generic setAddressValue method.

    if (this.state.outputs.has(math.outputAddress)) {
        const out = this.state.outputs.get(math.outputAddress)!;
        this.state.outputs.set(math.outputAddress, { ...out, value: result, lastUpdated: this.state.cycle });
    } else if (this.state.memory.has(math.outputAddress)) {
        const mem = this.state.memory.get(math.outputAddress)!;
        this.state.memory.set(math.outputAddress, { ...mem, value: result });
    } else {
        this.state.memory.set(math.outputAddress, { address: math.outputAddress, value: result });
    }

    return true; // Math element itself is "true" if it doesn't error and is evaluated in a series
  }

  private getValueFromAddress(address: string): boolean | number {
    if (this.state.inputs.has(address)) return this.getInput(address);
    if (this.state.memory.has(address)) return this.getMemory(address);
    if (this.state.outputs.has(address)) return this.getOutput(address);
    throw new InvalidMemoryAccessError(address);
  }

  // ─── New Instruction Evaluators ─────────────────────────────────

  /** One-Shot: fires true for exactly one scan on the specified edge. */
  private evaluateOneShot(oneshot: OneShot, rungState: boolean, lastRungState: boolean): boolean {
    if (!rungState) return false;

    const currentValue = this.getInput(oneshot.address) || this.getMemory(oneshot.address) || this.getOutput(oneshot.address);
    const prevValue = this.state.previousInputs.get(oneshot.address);
    const currentBool = !!currentValue;
    const prevBool = prevValue !== undefined ? !!prevValue : false;

    if (oneshot.edgeType === 'RISING') {
      return currentBool && !prevBool;
    } else {
      // FALLING
      return !currentBool && prevBool;
    }
  }

  /** MOV: copies value from source to destination. */
  private evaluateMove(move: MoveElement, rungState: boolean): boolean {
    if (!rungState) return false;

    const sourceValue = this.getValueFromAddress(move.source);

    if (this.state.outputs.has(move.destination)) {
      const out = this.state.outputs.get(move.destination)!;
      this.state.outputs.set(move.destination, { ...out, value: sourceValue, lastUpdated: this.state.cycle });
    } else if (this.state.memory.has(move.destination)) {
      this.state.memory.set(move.destination, {
        address: move.destination,
        value: sourceValue,
      });
    } else {
      this.state.memory.set(move.destination, { address: move.destination, value: sourceValue });
    }

    return true;
  }

  /** Scale: scales input from one range to another. */
  private evaluateScale(scale: ScaleElement, rungState: boolean): boolean {
    if (!rungState) return false;

    const inputVal = this.getValueFromAddress(scale.inputAddress);
    if (typeof inputVal !== 'number') {
      throw new MathOperationError('SLC', 'Input must be a number');
    }

    const range = scale.inMax - scale.inMin;
    let scaled: number;
    if (range === 0) {
      scaled = scale.outMin;
    } else {
      scaled = scale.outMin + ((inputVal - scale.inMin) / range) * (scale.outMax - scale.outMin);
    }
    // Clamp to output range
    scaled = Math.max(scale.outMin, Math.min(scale.outMax, scaled));

    if (this.state.outputs.has(scale.destination)) {
      const out = this.state.outputs.get(scale.destination)!;
      this.state.outputs.set(scale.destination, { ...out, value: scaled, lastUpdated: this.state.cycle });
    } else if (this.state.memory.has(scale.destination)) {
      this.state.memory.set(scale.destination, { address: scale.destination, value: scaled });
    } else {
      this.state.memory.set(scale.destination, { address: scale.destination, value: scaled });
    }

    return true;
  }

  /** Compare: evaluate inputA op inputB, write boolean result to outputAddress. */
  private evaluateCompare(cmp: CompareElement, rungState: boolean): boolean {
    if (!rungState) return false;

    const valA = this.getValueFromAddress(cmp.inputA);
    const valB = this.getValueFromAddress(cmp.inputB);

    if (typeof valA !== 'number' || typeof valB !== 'number') {
      throw new MathOperationError('CMP', 'Both inputs must be numbers');
    }

    const result = this.compare(valA, cmp.op, valB);

    if (this.state.outputs.has(cmp.outputAddress)) {
      const out = this.state.outputs.get(cmp.outputAddress)!;
      this.state.outputs.set(cmp.outputAddress, { ...out, value: result, lastUpdated: this.state.cycle });
    } else if (this.state.memory.has(cmp.outputAddress)) {
      this.state.memory.set(cmp.outputAddress, { address: cmp.outputAddress, value: result });
    } else {
      this.state.memory.set(cmp.outputAddress, { address: cmp.outputAddress, value: result });
    }

    return result;
  }

  /** Takes a deep snapshot of the current engine state. */
  takeSnapshot(): EngineState {
    return {
      cycle: this.state.cycle,
      inputs: new Map(Array.from(this.state.inputs.entries()).map(([k, v]) => [k, { ...v }])),
      outputs: new Map(Array.from(this.state.outputs.entries()).map(([k, v]) => [k, { ...v }])),
      memory: new Map(Array.from(this.state.memory.entries()).map(([k, v]) => [k, { ...v }])),
      timers: new Map(Array.from(this.state.timers.entries()).map(([k, v]) => [k, { ...v }])),
      counters: new Map(Array.from(this.state.counters.entries()).map(([k, v]) => [k, { ...v }])),
      previousInputs: new Map(this.state.previousInputs),
      running: this.state.running,
    };
  }

  /** Restores the engine state from a snapshot. */
  restoreSnapshot(snapshot: EngineState): void {
    this.state = {
      cycle: snapshot.cycle,
      inputs: new Map(Array.from(snapshot.inputs.entries()).map(([k, v]) => [k, { ...v }])),
      outputs: new Map(Array.from(snapshot.outputs.entries()).map(([k, v]) => [k, { ...v }])),
      memory: new Map(Array.from(snapshot.memory.entries()).map(([k, v]) => [k, { ...v }])),
      timers: new Map(Array.from(snapshot.timers.entries()).map(([k, v]) => [k, { ...v }])),
      counters: new Map(Array.from(snapshot.counters.entries()).map(([k, v]) => [k, { ...v }])),
      previousInputs: new Map(snapshot.previousInputs),
      running: snapshot.running,
    };
    this.notify();
  }

  private compare(a: number, op: string, b: number): boolean {
    switch (op) {
      case '==': return a === b;
      case '!=': return a !== b;
      case '>': return a > b;
      case '<': return a < b;
      case '>=': return a >= b;
      case '<=': return a <= b;
      default: return false;
    }
  }

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

  private findComponentRecursive(
    elements: RungElement[],
    componentId: string,
  ): { parent: RungElement[]; index: number; element: RungElement } | null {
    for (let i = 0; i < elements.length; i++) {
      const element = elements[i];
      if ((element as any).id === componentId) {
        return { parent: elements, index: i, element };
      }
      if (element.type === 'branch') {
        const branch = element as Branch;
        for (const path of branch.paths) {
          const found = this.findComponentRecursive(path, componentId);
          if (found) return found;
        }
      }
    }
    return null;
  }

  private reindexSeries(elements: RungElement[]): void {
    elements.forEach((el, i) => {
      (el as any).position = i;
      if (el.type === 'branch') {
        (el as Branch).paths.forEach((path) => this.reindexSeries(path));
      }
    });
  }

  private traverseRung(rung: Rung, callback: (el: RungElement) => void): void {
    this.traverseElements(rung.series, callback);
  }

  private traverseElements(
    elements: RungElement[],
    callback: (el: RungElement) => void,
  ): void {
    for (const el of elements) {
      callback(el);
      if (el.type === 'branch') {
        (el as Branch).paths.forEach((path) =>
          this.traverseElements(path, callback),
        );
      }
    }
  }
}
