/**
 * Push3 parser — converts Push3 source text into an AST.
 *
 * Node types:
 *   { kind: 'int',  value: bigint }       — integer literal (1e18 etc.)
 *   { kind: 'bool', value: boolean }      — TRUE / FALSE
 *   { kind: 'instr', name: string }       — DYADIC.+, EXEC.IF, etc.
 *   { kind: 'name',  text: string }       — unbound identifier (TAXRATE etc.)
 *   { kind: 'list',  items: Node[] }      — ( ... )
 */

export type Node =
  | { kind: 'int';   value: bigint }
  | { kind: 'bool';  value: boolean }
  | { kind: 'instr'; name: string }
  | { kind: 'name';  text: string }
  | { kind: 'list';  items: Node[] };

// Known instruction prefixes / exact names
const KNOWN_INSTR_PREFIXES = [
  'DYADIC.', 'EXEC.', 'BOOLEAN.', 'CODE.', 'NAME.', 'INDEX.',
  'INTVECTOR.', 'FLOATVECTOR.', 'BOOLVECTOR.', 'GRAPH.',
];

function isInstruction(token: string): boolean {
  for (const pfx of KNOWN_INSTR_PREFIXES) {
    if (token.startsWith(pfx)) return true;
  }
  return false;
}

function tokenize(src: string): string[] {
  // Strip comments (;; to end of line)
  const noComments = src.replace(/;;[^\n]*/g, ' ');
  // Split on whitespace, treating ( and ) as separate tokens
  const spaced = noComments.replace(/\(/g, ' ( ').replace(/\)/g, ' ) ');
  return spaced.trim().split(/\s+/).filter(t => t.length > 0);
}

function parseTokens(tokens: string[], pos: number): [Node, number] {
  const token = tokens[pos];
  if (token === undefined) throw new Error('Unexpected end of tokens');

  if (token === '(') {
    // Parse list until matching ')'
    const items: Node[] = [];
    let i = pos + 1;
    while (i < tokens.length && tokens[i] !== ')') {
      const [node, next] = parseTokens(tokens, i);
      items.push(node);
      i = next;
    }
    if (tokens[i] !== ')') throw new Error('Unmatched (');
    return [{ kind: 'list', items }, i + 1];
  }

  if (token === 'TRUE')  return [{ kind: 'bool', value: true },  pos + 1];
  if (token === 'FALSE') return [{ kind: 'bool', value: false }, pos + 1];

  // Integer literal — may be large (BigInt)
  if (/^-?\d+$/.test(token)) {
    return [{ kind: 'int', value: BigInt(token) }, pos + 1];
  }

  if (isInstruction(token)) {
    return [{ kind: 'instr', name: token }, pos + 1];
  }

  // Otherwise: unbound name (e.g. TAXRATE, STAKED, DELTAS, EFFIDX)
  return [{ kind: 'name', text: token }, pos + 1];
}

export function parse(src: string): Node {
  const tokens = tokenize(src);
  if (tokens.length === 0) throw new Error('Empty program');
  const [node, consumed] = parseTokens(tokens, 0);
  if (consumed !== tokens.length) {
    throw new Error(`Unexpected tokens after position ${consumed}: ${tokens[consumed]}`);
  }
  return node;
}
