// Small, safe expression evaluator for panel logic.
//
// Supports numbers, booleans (true/false → 1/0), arithmetic (+ - * / %),
// comparison (< <= > >= == !=), boolean (&& || !), ternary (a ? b : c),
// parentheses, array literals ([a, b, c]), postfix indexing (arr[i]),
// and a set of math + array functions. Two kinds of variable reference are
// resolved live at evaluation time:
//   {ds:name}   a data-source signal value (the brace content is split on the
//               FIRST ':' so EPICS PV names like "MY:PV:NAME" work).
//   bareIdent   a panel-local state variable (data source 'local').
//
// Booleans are represented as numbers: comparisons / logical ops yield 1 or 0,
// and any nonzero value is truthy. The evaluator never touches the DOM or eval;
// it walks a parsed AST against a caller-supplied resolver.

import type { ArrVal } from './arraypolicy';

export interface RefLite { ds: string; name: string }

type Node =
  | { t: 'num'; v: number }
  | { t: 'sig'; ds: string; name: string }
  | { t: 'var'; name: string }
  | { t: 'un'; op: string; a: Node }
  | { t: 'bin'; op: string; a: Node; b: Node }
  | { t: 'tern'; c: Node; a: Node; b: Node }
  | { t: 'call'; fn: string; args: Node[] }
  | { t: 'arr'; items: Node[] }
  | { t: 'index'; a: Node; i: Node };

const SCALAR_FUNCS: Record<string, (a: number[]) => number> = {
  abs: a => Math.abs(a[0]),
  sqrt: a => Math.sqrt(a[0]),
  floor: a => Math.floor(a[0]),
  ceil: a => Math.ceil(a[0]),
  round: a => Math.round(a[0]),
  sign: a => Math.sign(a[0]),
  pow: a => Math.pow(a[0], a[1]),
  log: a => Math.log(a[0]),
  exp: a => Math.exp(a[0]),
  sin: a => Math.sin(a[0]),
  cos: a => Math.cos(a[0]),
};

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

interface Tok { k: string; v?: string }

function tokenize(src: string): Tok[] {
  const toks: Tok[] = [];
  let i = 0;
  const two = ['<=', '>=', '==', '!=', '&&', '||'];
  while (i < src.length) {
    const c = src[i];
    if (c === ' ' || c === '\t' || c === '\n' || c === '\r') { i++; continue; }
    // signal ref {ds:name}
    if (c === '{') {
      const end = src.indexOf('}', i);
      if (end < 0) throw new Error('unterminated { in expression');
      toks.push({ k: 'sig', v: src.slice(i + 1, end) });
      i = end + 1;
      continue;
    }
    // number
    if ((c >= '0' && c <= '9') || (c === '.' && /[0-9]/.test(src[i + 1] ?? ''))) {
      let j = i + 1;
      while (j < src.length && /[0-9.]/.test(src[j])) j++;
      toks.push({ k: 'num', v: src.slice(i, j) });
      i = j;
      continue;
    }
    // identifier
    if (/[A-Za-z_]/.test(c)) {
      let j = i + 1;
      while (j < src.length && /[A-Za-z0-9_]/.test(src[j])) j++;
      toks.push({ k: 'ident', v: src.slice(i, j) });
      i = j;
      continue;
    }
    // two-char operators
    const pair = src.slice(i, i + 2);
    if (two.includes(pair)) { toks.push({ k: pair }); i += 2; continue; }
    // single-char operators / punctuation (including brackets)
    if ('+-*/%<>!()?:,[]'.includes(c)) { toks.push({ k: c }); i++; continue; }
    throw new Error(`unexpected character '${c}' in expression`);
  }
  return toks;
}

// ── Parser (recursive descent) ────────────────────────────────────────────────

function parse(src: string): Node {
  const toks = tokenize(src);
  let p = 0;
  const peek = () => toks[p];
  const eat = (k?: string): Tok => {
    const t = toks[p];
    if (!t || (k && t.k !== k)) throw new Error(`expected '${k}' in expression`);
    p++;
    return t;
  };

  function atom(): Node {
    const t = peek();
    if (!t) throw new Error('unexpected end of expression');
    if (t.k === 'num') { eat(); return { t: 'num', v: parseFloat(t.v!) }; }
    if (t.k === '[') {
      eat('[');
      const items: Node[] = [];
      if (peek()?.k !== ']') {
        items.push(ternary());
        while (peek()?.k === ',') { eat(','); items.push(ternary()); }
      }
      eat(']');
      return { t: 'arr', items };
    }
    if (t.k === 'sig') {
      eat();
      const raw = t.v!;
      const idx = raw.indexOf(':');
      const ds = idx < 0 ? raw : raw.slice(0, idx);
      const name = idx < 0 ? '' : raw.slice(idx + 1);
      return { t: 'sig', ds, name };
    }
    if (t.k === 'ident') {
      eat();
      const id = t.v!;
      if (id === 'true') return { t: 'num', v: 1 };
      if (id === 'false') return { t: 'num', v: 0 };
      if (peek()?.k === '(') {
        eat('(');
        const args: Node[] = [];
        if (peek()?.k !== ')') {
          args.push(ternary());
          while (peek()?.k === ',') { eat(','); args.push(ternary()); }
        }
        eat(')');
        return { t: 'call', fn: id, args };
      }
      return { t: 'var', name: id };
    }
    if (t.k === '(') { eat('('); const e = ternary(); eat(')'); return e; }
    throw new Error(`unexpected token '${t.k}' in expression`);
  }

  function primary(): Node {
    let n = atom();
    while (peek()?.k === '[') {
      eat('[');
      const i = ternary();
      eat(']');
      n = { t: 'index', a: n, i };
    }
    return n;
  }

  function unary(): Node {
    const t = peek();
    if (t && (t.k === '-' || t.k === '!')) { eat(); return { t: 'un', op: t.k, a: unary() }; }
    return primary();
  }

  function bin(next: () => Node, ops: string[]): () => Node {
    return () => {
      let a = next();
      while (peek() && ops.includes(peek().k)) {
        const op = eat().k;
        a = { t: 'bin', op, a, b: next() };
      }
      return a;
    };
  }

  const mul = bin(unary, ['*', '/', '%']);
  const add = bin(mul, ['+', '-']);
  const cmp = bin(add, ['<', '<=', '>', '>=']);
  const eq = bin(cmp, ['==', '!=']);
  const and = bin(eq, ['&&']);
  const or = bin(and, ['||']);

  function ternary(): Node {
    const c = or();
    if (peek()?.k === '?') {
      eat('?');
      const a = ternary();
      eat(':');
      const b = ternary();
      return { t: 'tern', c, a, b };
    }
    return c;
  }

  const root = ternary();
  if (p < toks.length) throw new Error('trailing tokens in expression');
  return root;
}

// Parsed-AST cache so the engine can re-evaluate hot expressions cheaply.
const cache = new Map<string, Node | Error>();

function parseCached(src: string): Node {
  let n = cache.get(src);
  if (n === undefined) {
    try { n = parse(src); } catch (e) { n = e as Error; }
    cache.set(src, n);
  }
  if (n instanceof Error) throw n;
  return n;
}

// ── Evaluation ─────────────────────────────────────────────────────────────

export type Resolver = (ds: string, name: string) => ArrVal;

function asNum(v: ArrVal): number {
  if (typeof v !== 'number') throw new Error('expected a number, got an array');
  return v;
}
function asArr(v: ArrVal): ArrVal[] {
  if (!Array.isArray(v)) throw new Error('expected an array, got a number');
  return v;
}
// resolve a possibly-negative index against length
function idx(i: number, len: number): number {
  const k = Math.trunc(i) < 0 ? len + Math.trunc(i) : Math.trunc(i);
  if (k < 0 || k >= len) throw new Error(`index ${i} out of range (len ${len})`);
  return k;
}

const ARR_FUNCS: Record<string, (a: ArrVal[]) => ArrVal> = {
  len: a => asArr(a[0]).length,
  sum: a => asArr(a[0]).reduce<number>((s, x) => s + asNum(x), 0),
  mean: a => { const r = asArr(a[0]); return r.length ? r.reduce<number>((s, x) => s + asNum(x), 0) / r.length : 0; },
  // min/max: scalar-variadic OR single-array — see ev() dispatch below
  slice: a => { const r = asArr(a[0]); const s = a[1] === undefined ? 0 : asNum(a[1]); const e = a[2] === undefined ? r.length : asNum(a[2]); return r.slice(s, e); },
  concat: a => asArr(a[0]).concat(asArr(a[1])),
  reverse: a => asArr(a[0]).slice().reverse(),
  sort: a => asArr(a[0]).slice().sort((x, y) => asNum(x) - asNum(y)),
  scale: a => asArr(a[0]).map(x => asNum(x) * asNum(a[1])),
  add: a => { const x = asArr(a[0]), y = asArr(a[1]); const n = Math.min(x.length, y.length); const o: ArrVal[] = []; for (let k = 0; k < n; k++) o.push(asNum(x[k]) + asNum(y[k])); return o; },
  sub: a => { const x = asArr(a[0]), y = asArr(a[1]); const n = Math.min(x.length, y.length); const o: ArrVal[] = []; for (let k = 0; k < n; k++) o.push(asNum(x[k]) - asNum(y[k])); return o; },
  push: a => asArr(a[0]).concat([a[1]]),
  set: a => { const r = asArr(a[0]).slice(); r[idx(asNum(a[1]), r.length)] = a[2]; return r; },
  insert: a => { const r = asArr(a[0]).slice(); const k = Math.max(0, Math.min(r.length, Math.trunc(asNum(a[1])))); r.splice(k, 0, a[2]); return r; },
  remove: a => { const r = asArr(a[0]).slice(); r.splice(idx(asNum(a[1]), r.length), 1); return r; },
  pop: a => { const r = asArr(a[0]).slice(); r.pop(); return r; },
  shift: a => { const r = asArr(a[0]).slice(); r.shift(); return r; },
  indexOf: a => { const r = asArr(a[0]); for (let k = 0; k < r.length; k++) if (r[k] === a[1]) return k; return -1; },
  contains: a => { const r = asArr(a[0]); for (let k = 0; k < r.length; k++) if (r[k] === a[1]) return 1; return 0; },
  fill: a => new Array(Math.max(0, Math.trunc(asNum(a[0])))).fill(a[1]),
};

function ev(n: Node, R: Resolver): ArrVal {
  switch (n.t) {
    case 'num': return n.v;
    case 'arr': return n.items.map(it => ev(it, R));
    case 'sig': return R(n.ds, n.name);
    case 'var': return R('local', n.name);
    case 'index': {
      const arr = asArr(ev(n.a, R));
      return arr[idx(asNum(ev(n.i, R)), arr.length)];
    }
    case 'un': return n.op === '-' ? -asNum(ev(n.a, R)) : (asNum(ev(n.a, R)) === 0 ? 1 : 0);
    case 'tern': return asNum(ev(n.c, R)) !== 0 ? ev(n.a, R) : ev(n.b, R);
    case 'call': {
      const args = n.args.map(a => ev(a, R));
      // min/max keep scalar-variadic form, plus 1-arg array form
      if ((n.fn === 'min' || n.fn === 'max') && !(args.length === 1 && Array.isArray(args[0]))) {
        const nums = args.map(asNum);
        return n.fn === 'min' ? Math.min(...nums) : Math.max(...nums);
      }
      if (n.fn === 'min' || n.fn === 'max') {
        const r = asArr(args[0]).map(asNum);
        return n.fn === 'min' ? Math.min(...r) : Math.max(...r);
      }
      const af = ARR_FUNCS[n.fn];
      if (af) return af(args);
      const sf = SCALAR_FUNCS[n.fn];
      if (sf) return sf(args.map(asNum));
      throw new Error(`unknown function '${n.fn}'`);
    }
    case 'bin': {
      const a = asNum(ev(n.a, R)), b = asNum(ev(n.b, R));
      switch (n.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 ? 1 : 0; case '<=': return a <= b ? 1 : 0;
        case '>': return a > b ? 1 : 0; case '>=': return a >= b ? 1 : 0;
        case '==': return a === b ? 1 : 0; case '!=': return a !== b ? 1 : 0;
        case '&&': return (a !== 0 && b !== 0) ? 1 : 0;
        case '||': return (a !== 0 || b !== 0) ? 1 : 0;
        default: throw new Error(`unknown operator '${n.op}'`);
      }
    }
  }
}

/** Evaluate an expression string; returns the full value (number or array). */
export function evalValue(src: string, resolve: Resolver): ArrVal {
  return ev(parseCached(src), resolve);
}

/** Evaluate an expression string. Returns NaN if it cannot be parsed/evaluated or the result is an array. */
export function evalExpr(src: string, resolve: Resolver): number {
  try {
    const v = ev(parseCached(src), resolve);
    return typeof v === 'number' ? v : NaN;
  } catch {
    return NaN;
  }
}

/** True if the expression evaluates to a truthy (nonzero, non-NaN) value. */
export function evalBool(src: string, resolve: Resolver): boolean {
  const v = evalExpr(src, resolve);
  return !isNaN(v) && v !== 0;
}

/** Collect every signal/local reference an expression reads, for subscription. */
export function collectRefs(src: string): RefLite[] {
  const out: RefLite[] = [];
  let root: Node;
  try { root = parseCached(src); } catch { return out; }
  const seen = new Set<string>();
  const add = (ds: string, name: string) => {
    const k = `${ds}\0${name}`;
    if (!seen.has(k)) { seen.add(k); out.push({ ds, name }); }
  };
  const walk = (n: Node) => {
    switch (n.t) {
      case 'sig': add(n.ds, n.name); break;
      case 'var': add('local', n.name); break;
      case 'un': walk(n.a); break;
      case 'bin': walk(n.a); walk(n.b); break;
      case 'tern': walk(n.c); walk(n.a); walk(n.b); break;
      case 'call': n.args.forEach(walk); break;
      case 'arr': n.items.forEach(walk); break;
      case 'index': walk(n.a); walk(n.i); break;
    }
  };
  walk(root);
  return out;
}

/** Validate an expression; returns an error message or null if it parses. */
export function checkExpr(src: string): string | null {
  if (!src.trim()) return null;
  try { parse(src); return null; } catch (e) { return e instanceof Error ? e.message : String(e); }
}
