/**
 * L5X RLL Importer — Allen Bradley Studio 5000 / Logix5000 format.
 *
 * Parses <RLLContent> rungs from an L5X XML file and converts them
 * into Lego Ladder Program objects.
 *
 * Supported mnemonics:
 *   XIC, XIO     → Contact (NO/NC)
 *   OTE, OTL, OTU → Coil
 *   TON, TOF, TP, RTO → Timer
 *   CTU, CTD, CTUD → Counter
 *   EQU, NEQ, GRT, LES, GEQ, LEQ → Compare
 *   AND, OR, XOR, NOT, NAND, NOR → LogicGate
 *   MOV           → MoveElement
 *   ADD, SUB, MUL, DIV → MathElement
 *
 * Rung text grammar:
 *   - Sequential ops: space-separated
 *   - Parallel branches: enclosed in [...] separated by commas
 *   - Semicolon terminates the rung
 */

import type {
  Program, Rung, RungElement, Contact, Coil, Timer, Counter,
  LogicGate, CompareElement, MoveElement, MathElement, Branch,
  CoilType, TimerType, CounterType, GateType, MathOperator
} from './types';

// ─── L5X instruction mapping ──────────────────────────────────────

const CONTACT_OPS = new Set(['XIC', 'XIO']);
const COIL_OPS = new Set(['OTE', 'OTL', 'OTU', 'SET', 'CLR']);
const TIMER_OPS = new Set(['TON', 'TOF', 'TP', 'RTO']);
const COUNTER_OPS = new Set(['CTU', 'CTD', 'CTUD']);
const COMPARE_OPS = new Set(['EQU', 'NEQ', 'GRT', 'LES', 'GEQ', 'LEQ']);
const GATE_OPS = new Set(['AND', 'OR', 'XOR', 'NOT', 'NAND', 'NOR']);
const MATH_OPS = new Set(['ADD', 'SUB', 'MUL', 'DIV']);

// L5X compare op → Lego Ladder ComparisonOp
const L5X_COMPARE_MAP: Record<string, string> = {
  EQU: '==',
  NEQ: '!=',
  GRT: '>',
  LES: '<',
  GEQ: '>=',
  LEQ: '<=',
};

// ─── Tokenizer ────────────────────────────────────────────────────

interface Token {
  op: string;
  args: string[];
}

function parseTokens(text: string): Token[] {
  text = text.replace(/^\s*<!\[CDATA\[/, '').replace(/\]\]>\s*$/, '').trim();
  const tokens: Token[] = [];
  let pos = 0;

  while (pos < text.length) {
    while (pos < text.length && /[,;\s]/.test(text[pos])) pos++;
    if (pos >= text.length) break;

    if (text[pos] === '[') {
      // Parallel branch group
      const branches: Token[][] = [];
      let depth = 0;
      let branchStart = pos + 1;

      for (let i = pos; i < text.length; i++) {
        if (text[i] === '[') depth++;
        else if (text[i] === ']') {
          depth--;
          if (depth === 0) {
            const branchText = text.substring(branchStart, i).trim();
            if (branchText) {
              const bTokens = parseBranchText(branchText);
              if (bTokens.length) branches.push(bTokens);
            }
            branchStart = i + 1;
          }
        } else if (text[i] === ',' && depth === 1) {
          const branchText = text.substring(branchStart, i).trim();
          if (branchText) {
            const bTokens = parseBranchText(branchText);
            if (bTokens.length) branches.push(bTokens);
          }
          branchStart = i + 1;
        }
      }

      const lastBranch = text.substring(branchStart, pos).trim();
      if (lastBranch) {
        const bTokens = parseBranchText(lastBranch);
        if (bTokens.length) branches.push(bTokens);
      }

      pos++;

      if (branches.length === 1) {
        tokens.push(...branches[0]);
      } else if (branches.length > 1) {
        // Flatten parallel branches — import all instructions
        for (const b of branches) tokens.push(...b);
      }
      continue;
    }

    const opMatch = /^([A-Z]+)\s*\(([^)]*)\)/.exec(text.substring(pos));
    if (opMatch) {
      const op = opMatch[1];
      const args = opMatch[2].split(',').map(a => a.trim()).filter(a => a !== '');
      tokens.push({ op, args });
      pos += opMatch[0].length;
    } else {
      pos++;
    }
  }

  return tokens;
}

function parseBranchText(text: string): Token[] {
  const parts: string[] = [];
  let depth = 0;
  let current = '';

  for (const ch of text) {
    if (ch === '(') depth++;
    else if (ch === ')') depth--;
    if (ch === ' ' && depth === 0) {
      if (current.trim()) parts.push(current.trim());
      current = '';
    } else {
      current += ch;
    }
  }
  if (current.trim()) parts.push(current.trim());

  const tokens: Token[] = [];
  for (const part of parts) {
    const m = /^([A-Z]+)\s*\(([^)]*)\)/.exec(part);
    if (m) {
      const args = m[2].split(',').map(a => a.trim()).filter(a => a !== '');
      tokens.push({ op: m[1], args });
    }
  }
  return tokens;
}

// ─── Token → RungElement ─────────────────────────────────────────

function tokenToElement(token: Token): RungElement | null {
  const { op, args } = token;

  if (CONTACT_OPS.has(op)) {
    return {
      type: 'contact',
      contactType: op === 'XIC' ? 'NO' : 'NC',
      address: args[0] || 'I:0/0',
    } as Contact;
  }

  if (COIL_OPS.has(op)) {
    let coilType: CoilType = 'OTE';
    if (op === 'OTL' || op === 'SET') coilType = 'SET';
    else if (op === 'OTU' || op === 'CLR') coilType = 'RESET';

    return {
      type: 'coil',
      coilType,
      address: args[0] || 'O:0/0',
    } as Coil;
  }

  if (TIMER_OPS.has(op)) {
    return {
      type: 'timer',
      timerType: op as TimerType,
      instanceId: args[0] || 'T4:0',
      preset: parseFloat(args[args.length - 1] || '0') * 1000, // L5X seconds → ms
      accumulated: 0,
    } as Timer;
  }

  if (COUNTER_OPS.has(op)) {
    return {
      type: 'counter',
      counterType: op as CounterType,
      instanceId: args[0] || 'C5:0',
      preset: parseInt(args[args.length - 1] || '0', 10),
      current: 0,
    } as Counter;
  }

  if (COMPARE_OPS.has(op)) {
    return {
      type: 'compare',
      op: L5X_COMPARE_MAP[op] || '==',
      inputA: args[0] || '0',
      inputB: args[1] || '0',
      outputAddress: args[2] || `CMP_${Date.now()}`,
    } as CompareElement;
  }

  if (GATE_OPS.has(op)) {
    return {
      type: 'gate',
      gateType: op as GateType,
      inputs: args.length >= 2 ? args.slice(0, 2) : [args[0] || 'I:0/0'],
      outputAddress: args[args.length - 1] || `GATE_${Date.now()}`,
    } as LogicGate;
  }

  if (MATH_OPS.has(op)) {
    return {
      type: 'math',
      operator: op as MathOperator,
      inputA: args[0] || '0',
      inputB: args[1] || '0',
      outputAddress: args[2] || 'N7:0',
    } as MathElement;
  }

  if (op === 'MOV') {
    return {
      type: 'move',
      source: args[0] || '0',
      destination: args[1] || 'N7:0',
    } as MoveElement;
  }

  // COP, BTD, and other instructions — skip for now
  return null;
}

// ─── Tokens → Rung ───────────────────────────────────────────────

function tokensToRung(tokens: Token[], rungNumber: number): Rung | null {
  if (tokens.length === 0) return null;

  const elements: RungElement[] = [];

  // Find last coil (output element)
  let coilIndex = -1;
  for (let i = tokens.length - 1; i >= 0; i--) {
    if (COIL_OPS.has(tokens[i].op)) {
      coilIndex = i;
      break;
    }
  }

  // Build series: conditions first, then coil at the end
  for (let i = 0; i < tokens.length; i++) {
    if (i === coilIndex) continue;
    const element = tokenToElement(tokens[i]);
    if (element) elements.push(element);
  }

  // Append coil last
  if (coilIndex >= 0) {
    const coil = tokenToElement(tokens[coilIndex]);
    if (coil) elements.push(coil);
  }

  if (elements.length === 0) return null;

  return {
    id: `rung-${rungNumber}`,
    label: `Rung ${rungNumber}`,
    series: elements,
    enabled: true,
  };
}

// ─── L5X XML Parsing ─────────────────────────────────────────────

export function parseL5X(l5xContent: string): Program[] {
  const programs: Program[] = [];

  // Extract RLL routines from <Program> blocks
  const programRegex = /<Program[^>]*>([\s\S]*?)<\/Program>/g;
  let programMatch;

  while ((programMatch = programRegex.exec(l5xContent)) !== null) {
    const fullMatch = programMatch[0];
    const programBlock = programMatch[1];
    const nameMatch = /<Program[^>]*Name="([^"]*)"/.exec(fullMatch);
    const programName = nameMatch?.[1] || 'Unknown';

    const rungs = parseRLLContent(programBlock);
    if (rungs.length > 0) {
      programs.push({
        name: programName,
        rungs,
        cycleTime: 100,
      });
    }
  }

  // Fallback: parse standalone <Routine> blocks
  if (programs.length === 0) {
    const routineRegex = /<Routine[^>]*Type="RLL"[^>]*>([\s\S]*?)<\/Routine>/g;
    let routineMatch;

    while ((routineMatch = routineRegex.exec(l5xContent)) !== null) {
      const routineBlock = routineMatch[1];
      const fullMatch = routineMatch[0];
      const nameMatch = /<Routine[^>]*Name="([^"]*)"/.exec(fullMatch || '');
      const routineName = nameMatch?.[1] || 'Imported_Routine';

      const rungs = parseRLLContent(routineBlock);
      if (rungs.length > 0) {
        programs.push({
          name: routineName,
          rungs,
          cycleTime: 100,
        });
      }
    }
  }

  return programs;
}

function parseRLLContent(block: string): Rung[] {
  const rungs: Rung[] = [];
  const rllMatch = /<RLLContent>([\s\S]*?)<\/RLLContent>/.exec(block);
  if (!rllMatch) return rungs;

  // Use a more robust regex that handles the 's' flag (dotall)
  const rungRegex = /<Rung\s+Number="(\d+)"[^>]*>([\s\S]*?)<\/Rung>/g;
  let rungMatch;

  while ((rungMatch = rungRegex.exec(rllMatch[1])) !== null) {
    const rungNumber = parseInt(rungMatch[1], 10);
    const rungBlock = rungMatch[2];

    const textMatch = /<!\[CDATA\[([\s\S]*)\]\]>/.exec(rungBlock);
    if (!textMatch) continue;

    const text = textMatch[1].trim();
    if (!text) continue;

    const tokens = parseTokens(text);
    if (tokens.length === 0) continue;

    const rung = tokensToRung(tokens, rungNumber);
    if (rung) rungs.push(rung);
  }

  return rungs;
}

// ─── Public API ───────────────────────────────────────────────────

export function importL5X(l5xContent: string): Program | null {
  const programs = parseL5X(l5xContent);
  return programs[0] || null;
}

export function importL5XAll(l5xContent: string): Program[] {
  return parseL5X(l5xContent);
}