#!/usr/bin/env node
/**
 * Generate a JavaScript DFA lexer from JSON tables (output of lexgen-tables).
 * Usage: node lexgen_code.js -o lexers/json_lexer.js -c JSONLexer [--prefix pgpg_] json-lex.json
 */
import fs from "fs";
import path from "path";

function loadTables(filePath) {
  return JSON.parse(fs.readFileSync(filePath, "utf-8"));
}

function buildTransitions(raw) {
  const transitions = raw.transitions ?? {};
  return Object.keys(transitions)
    .sort((a, b) => Number(a) - Number(b))
    .map((stateStr) => ({
      state: Number(stateStr),
      ranges: transitions[stateStr].map((tr) => ({
        from: tr.from,
        to: tr.to,
        next: tr.next,
      })),
    }));
}

function buildActions(raw) {
  const actions = raw.actions ?? {};
  return Object.keys(actions)
    .sort((a, b) => Number(a) - Number(b))
    .map((stateStr) => ({
      state: Number(stateStr),
      tokenType: actions[stateStr],
    }));
}

function serializeTransitions(transitions) {
  return transitions
    .map(
      (t) =>
        `  ${t.state}: [\n${t.ranges.map((r) => `    [${r.from}, ${r.to}, ${r.next}]`).join(",\n")}\n  ]`,
    )
    .join(",\n");
}

function serializeActions(actions) {
  return actions
    .map((a) => `  ${a.state}: ${JSON.stringify(a.tokenType)}`)
    .join(",\n");
}

function renderLexer(ctx) {
  const { className, startState, hasIgnored, transitions, actions } = ctx;
  const transitionsStr = serializeTransitions(transitions);
  const actionsStr = serializeActions(actions);
  const ignoredCheck = hasIgnored
    ? `
      if (tokenType.startsWith("!")) continue;`
    : "";
  return `/**
 * Generated by lexgen_code.js from lexgen JSON tables. Do not edit.
 */
import { newToken, newEOFToken, newErrorToken, Location } from "../../../generators/js/runtime/index.js";

const START_STATE = ${startState};

const TRANSITIONS = {
${transitionsStr}
};

const ACTIONS = {
${actionsStr}
};

function lookupTransition(state, r) {
  const ranges = TRANSITIONS[state];
  if (!ranges) return null;
  for (const [from, to, next] of ranges) {
    if (r < from) return null;
    if (r >= from && r <= to) return next;
  }
  return null;
}

export class ${className} {
  constructor(inputText) {
    this._input = inputText;
    this._length = inputText.length;
    this._index = 0;
    this._line = 1;
    this._column = 1;
  }

  scan() {
    for (;;) {
      if (this._index >= this._length)
        return newEOFToken(new Location(this._line, this._column, this._index));

      const startLine = this._line;
      const startCol = this._column;
      const startIndex = this._index;
      let state = START_STATE;
      let lastAcceptState = null;
      let lastAcceptIndex = this._index;
      let lastAcceptLine = this._line;
      let lastAcceptCol = this._column;

      while (this._index < this._length) {
        const r = this._input.codePointAt(this._index);
        const nextState = lookupTransition(state, r);
        if (nextState == null) break;
        this._index += r > 0xffff ? 2 : 1;
        if (this._input[this._index - 1] === "\\n") {
          this._line++;
          this._column = 1;
        } else this._column++;
        state = nextState;
        if (ACTIONS[state] !== undefined) {
          lastAcceptState = state;
          lastAcceptIndex = this._index;
          lastAcceptLine = this._line;
          lastAcceptCol = this._column;
        }
      }

      if (lastAcceptState == null) {
        const r = this._index < this._length ? this._input.codePointAt(this._index) : 0;
        return newErrorToken(
          \`lexer: unrecognized input \${String.fromCodePoint(r)}\`,
          new Location(this._line, this._column, this._index),
        );
      }

      const lexeme = this._input.slice(startIndex, lastAcceptIndex);
      this._index = lastAcceptIndex;
      this._line = lastAcceptLine;
      this._column = lastAcceptCol;
      const tokenType = ACTIONS[lastAcceptState];${ignoredCheck}
      return newToken(lexeme, tokenType, new Location(startLine, startCol, startIndex));
    }
  }
}
`;
}

function parseArgs(argv) {
  const args = { output: null, className: null, prefix: "pgpg_", jsonFile: null };
  for (let i = 0; i < argv.length; i++) {
    if (argv[i] === "-o" && argv[i + 1]) args.output = argv[++i];
    else if (argv[i] === "-c" && argv[i + 1]) args.className = argv[++i];
    else if (argv[i] === "--prefix" && argv[i + 1]) args.prefix = argv[++i];
    else if (!argv[i].startsWith("-")) args.jsonFile = argv[i];
  }
  return args;
}

function main() {
  const args = parseArgs(process.argv.slice(2));
  if (!args.output || !args.className || !args.jsonFile) {
    console.error("Usage: node lexgen_code.js -o <output.js> -c <ClassName> [--prefix pgpg_] <lex.json>");
    process.exit(1);
  }
  const raw = loadTables(args.jsonFile);
  const startState = raw.start_state ?? 0;
  const transitions = buildTransitions(raw);
  const actions = buildActions(raw);
  const hasIgnored = actions.some((a) => a.tokenType.startsWith("!"));
  const className = args.prefix ? args.prefix + args.className : args.className;
  fs.mkdirSync(path.dirname(args.output), { recursive: true });
  fs.writeFileSync(
    args.output,
    renderLexer({ className, startState, hasIgnored, transitions, actions }),
    "utf-8",
  );
}

main();
