/**
 * Expression Evaluator — mirrors FaceExpr.kt
 * Supports: math (+,-,*,/), parentheses, functions (clamp, min, max, abs, round, floor, ceil),
 * variables ($progress, $raw, etc.), custom @var, compound conditions (&&, ||),
 * inline-if: iif(condition,trueVal,falseVal).
 */

function substituteVars(expr, ctx) {
  let r = expr;
  // Custom vars first (longest match first)
  if (ctx._customVars) {
    const sorted = Object.entries(ctx._customVars).sort((a, b) => b[0].length - a[0].length);
    for (const [name, val] of sorted) {
      r = r.replaceAll(name, String(val));
    }
  }
  r = r.replace(/\$progress/g, String(ctx.progress));
  r = r.replace(/\$percent/g, String(ctx.progress * 100));
  r = r.replace(/\$raw/g, String(ctx.rawValue ?? 0));
  r = r.replace(/\$min/g, String(ctx.minValue));
  r = r.replace(/\$max/g, String(ctx.maxValue));
  r = r.replace(/\$whp/g, String(ctx.warnHighProgress ?? 1));
  r = r.replace(/\$wlp/g, String(ctx.warnLowProgress ?? 0));
  r = r.replace(/\$dhp/g, String(ctx.dangerHighProgress ?? 1));
  r = r.replace(/\$dlp/g, String(ctx.dangerLowProgress ?? 0));
  r = r.replace(/\$time/g, String((performance.now() % 3600000) / 1000));
  return r;
}

function evalMath(expr) {
  const cleaned = expr.replace(/\s/g, '');
  try {
    const pos = [0];
    return parseAddSub(cleaned, pos);
  } catch(e) {
    const n = parseFloat(cleaned);
    return isNaN(n) ? 0 : n;
  }
}
function parseAddSub(e, p) {
  let r = parseMulDiv(e, p);
  while (p[0] < e.length && (e[p[0]] === '+' || e[p[0]] === '-')) {
    const op = e[p[0]++];
    const rhs = parseMulDiv(e, p);
    r = op === '+' ? r + rhs : r - rhs;
  }
  return r;
}
function parseMulDiv(e, p) {
  let r = parseAtom(e, p);
  while (p[0] < e.length && (e[p[0]] === '*' || e[p[0]] === '/')) {
    const op = e[p[0]++];
    const rhs = parseAtom(e, p);
    r = op === '*' ? r * rhs : (rhs !== 0 ? r / rhs : 0);
  }
  return r;
}
function parseAtom(e, p) {
  // Parentheses
  if (p[0] < e.length && e[p[0]] === '(') {
    p[0]++;
    const r = parseAddSub(e, p);
    if (p[0] < e.length && e[p[0]] === ')') p[0]++;
    return r;
  }
  // Function call: name(args...)
  const funcStart = p[0];
  if (p[0] < e.length && isAlpha(e[p[0]])) {
    while (p[0] < e.length && (isAlpha(e[p[0]]) || isDigit(e[p[0]]))) p[0]++;
    const fname = e.substring(funcStart, p[0]);
    if (p[0] < e.length && e[p[0]] === '(') {
      p[0]++;
      const args = [];
      if (p[0] < e.length && e[p[0]] !== ')') {
        args.push(parseAddSub(e, p));
        while (p[0] < e.length && e[p[0]] === ',') {
          p[0]++;
          args.push(parseAddSub(e, p));
        }
      }
      if (p[0] < e.length && e[p[0]] === ')') p[0]++;
      return callFunc(fname, args);
    }
    p[0] = funcStart; // Not a function, reset
  }
  // Unary minus
  const neg = p[0] < e.length && e[p[0]] === '-';
  if (neg) p[0]++;
  const start = p[0];
  while (p[0] < e.length && (isDigit(e[p[0]]) || e[p[0]] === '.')) p[0]++;
  // Scientific notation (e.g., 2.5E-16, 1e10)
  if (p[0] > start && p[0] < e.length && (e[p[0]] === 'E' || e[p[0]] === 'e')) {
    p[0]++;
    if (p[0] < e.length && (e[p[0]] === '+' || e[p[0]] === '-')) p[0]++;
    while (p[0] < e.length && isDigit(e[p[0]])) p[0]++;
  }
  const num = p[0] > start ? parseFloat(e.substring(start, p[0])) || 0 : 0;
  return neg ? -num : num;
}

function callFunc(name, args) {
  const a = args[0] || 0, b = args[1] || 0, c = args[2] || 0;
  switch (name) {
    case 'clamp': return Math.min(Math.max(a, b), c);
    case 'min': return Math.min(a, b);
    case 'max': return Math.max(a, b);
    case 'abs': return Math.abs(a);
    case 'round': return Math.round(a);
    case 'floor': return Math.floor(a);
    case 'ceil': return Math.ceil(a);
    case 'lerp': return a + (b - a) * c;
    case 'sin': return Math.sin(a * Math.PI / 180);
    case 'cos': return Math.cos(a * Math.PI / 180);
    case 'mod': return b !== 0 ? ((a % b) + b) % b : 0;
    case 'pow': return Math.pow(a, b);
    default: return 0;
  }
}

function isDigit(c) { return c >= '0' && c <= '9'; }
function isAlpha(c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c === '_'; }

/** Resolve inline-if: iif(condition,trueVal,falseVal) */
function resolveIif(expr, ctx) {
  if (!expr || !expr.startsWith('iif(') || !expr.endsWith(')')) return expr;
  const inner = expr.substring(4, expr.length - 1);
  const parts = [];
  let depth = 0, start = 0;
  for (let i = 0; i < inner.length; i++) {
    if (inner[i] === '(') depth++;
    else if (inner[i] === ')') depth--;
    else if (inner[i] === ',' && depth === 0) {
      parts.push(inner.substring(start, i).trim());
      start = i + 1;
    }
  }
  parts.push(inner.substring(start).trim());
  if (parts.length !== 3) return expr;
  const result = evalCondition(parts[0], ctx) ? parts[1] : parts[2];
  return resolveIif(result, ctx);
}

function resolveFloat(expr, ctx) {
  return evalMath(substituteVars(resolveIif(expr, ctx), ctx));
}

/** Try to resolve as float; returns NaN if expression is non-numeric (color, text). */
function tryResolveFloat(expr, ctx) {
  const iifResolved = resolveIif(expr, ctx);
  const substituted = substituteVars(iifResolved, ctx);
  const cleaned = substituted.replace(/\s/g, '');
  if (cleaned === '') return NaN;
  try {
    const pos = [0];
    const result = parseAddSub(cleaned, pos);
    return pos[0] >= cleaned.length ? result : NaN;
  } catch(e) {
    return parseFloat(cleaned);
  }
}

function resolveColor(expr, ctx) {
  const resolved = resolveIif(expr, ctx);
  if (resolved !== expr) return resolveColor(resolved, ctx);
  // Substitute custom vars (for $border_color → iif(...) or #hex)
  if (ctx._customVars && expr.includes('$')) {
    let sub = expr;
    const sorted = Object.entries(ctx._customVars).sort((a, b) => b[0].length - a[0].length);
    for (const [n, v] of sorted) { sub = sub.replaceAll(n, v); }
    if (sub !== expr) return resolveColor(sub, ctx);
  }
  switch (expr) {
    case '$accent': return ctx.accentColor;
    case '$zone_color': return zoneColor(ctx);
    case 'white': return '#FFFFFF';
    case 'black': return '#000000';
    case 'muted': return '#888888';
    case 'warning': return '#FFD96B';
    case 'danger': return '#FF6B6B';
    case 'success': return '#4CAF50';
    case 'red': return '#FF0000';
    case 'green': return '#00FF00';
    case 'blue': return '#0000FF';
    case 'yellow': return '#FFFF00';
    case 'cyan': return '#00FFFF';
    case 'orange': return '#FF9800';
    default:
      if (expr && expr.startsWith('#')) return parseHexColor(expr);
      return '#FFFFFF';
  }
}

function zoneColor(ctx) {
  if (ctx.zone === 'danger') return '#FF6B6B';
  if (ctx.zone === 'warning') return '#FFD96B';
  return ctx.accentColor;
}

function parseHexColor(hex) {
  const c = hex.replace('#', '');
  if (c.length === 3) return '#' + c[0]+c[0]+c[1]+c[1]+c[2]+c[2];
  if (c.length === 8) {
    const a = parseInt(c.substring(0,2),16)/255;
    return `rgba(${parseInt(c.substring(2,4),16)},${parseInt(c.substring(4,6),16)},${parseInt(c.substring(6,8),16)},${a.toFixed(3)})`;
  }
  if (c.length === 6) return '#' + c;
  return '#FFFFFF';
}

function resolveText(expr, ctx) {
  const resolved = resolveIif(expr, ctx);
  if (resolved !== expr) return resolveText(resolved, ctx);
  // Substitute custom vars (for $my_text → iif(...) or literal)
  if (ctx._customVars && expr.includes('$')) {
    let sub = expr;
    const sorted = Object.entries(ctx._customVars).sort((a, b) => b[0].length - a[0].length);
    for (const [n, v] of sorted) { sub = sub.replaceAll(n, v); }
    if (sub !== expr) return resolveText(sub, ctx);
  }
  switch (expr) {
    case '$value': return ctx.formattedValue;
    case '$unit': return ctx.unit;
    case '$label': return ctx.label;
    case '$min': return fmtNum(ctx.minValue);
    case '$max': return fmtNum(ctx.maxValue);
    case '$raw': return ctx.rawValue != null ? fmtNum(ctx.rawValue) : '--';
    case '$percent': return Math.round(ctx.progress * 100) + '%';
    default:
      let text = expr.replace(/^"|"$/g, '');
      if (text.includes('$')) {
        text = text.replace(/\$value/g, ctx.formattedValue);
        text = text.replace(/\$unit/g, ctx.unit);
        text = text.replace(/\$label/g, ctx.label);
        text = text.replace(/\$percent/g, Math.round(ctx.progress * 100) + '%');
        text = text.replace(/\$min/g, fmtNum(ctx.minValue));
        text = text.replace(/\$max/g, fmtNum(ctx.maxValue));
        text = text.replace(/\$raw/g, ctx.rawValue != null ? fmtNum(ctx.rawValue) : '--');
      }
      return text;
  }
}

function fmtNum(v) {
  if (v === Math.floor(v)) return String(Math.floor(v));
  return v.toFixed(1);
}

function evalCondition(condition, ctx) {
  const s = condition.trim();
  // Handle || (OR) - lowest priority
  if (s.includes('||')) {
    return s.split('||').some(part => evalCondition(part, ctx));
  }
  // Handle && (AND)
  if (s.includes('&&')) {
    return s.split('&&').every(part => evalCondition(part, ctx));
  }
  // Single condition
  const ops = ['!=','==','>=','<=','>','<'];
  for (const op of ops) {
    if (s.includes(op)) {
      const idx = s.indexOf(op);
      const l = s.substring(0, idx).trim();
      const r = s.substring(idx + op.length).trim();
      if (op === '==' || op === '!=') {
        // Try numeric comparison first (handles $progress==0, $raw>=50, etc.)
        const lf = tryResolveFloat(l, ctx), rf = tryResolveFloat(r, ctx);
        if (!isNaN(lf) && !isNaN(rf)) {
          return op === '==' ? lf === rf : lf !== rf;
        }
        // Fall back to string comparison (handles $zone==danger, etc.)
        const lv = resolveString(l, ctx), rv = resolveString(r, ctx);
        return op === '==' ? lv === rv : lv !== rv;
      }
      const lv = resolveFloat(l, ctx), rv = resolveFloat(r, ctx);
      if (op === '>=') return lv >= rv;
      if (op === '<=') return lv <= rv;
      if (op === '>') return lv > rv;
      if (op === '<') return lv < rv;
    }
  }
  return false;
}

function resolveString(expr, ctx) {
  if (expr === '$zone') return ctx.zone;
  return expr;
}

/** Resolve @var declarations and inject into context.
 *  Numeric vars are stored as number strings ("42.5"),
 *  non-numeric vars (colors, iif returning colors) are stored as raw expressions. */
function resolveCustomVars(face, ctx) {
  if (!face.vars || Object.keys(face.vars).length === 0) return ctx;
  // Seed with existing _customVars (e.g. from data bindings) so they are available to @var expressions
  const seedVars = ctx._customVars ? Object.assign({}, ctx._customVars) : {};
  const extended = Object.assign({}, ctx, { _customVars: seedVars });
  for (const [name, expr] of Object.entries(face.vars)) {
    // Substitute already-resolved custom vars
    let resolved = String(expr);
    const sorted = Object.entries(extended._customVars).sort((a, b) => b[0].length - a[0].length);
    for (const [n, v] of sorted) { resolved = resolved.replaceAll(n, v); }
    // Try to evaluate as number; if non-numeric, store raw expression
    const f = tryResolveFloat(resolved, extended);
    extended._customVars[name] = isNaN(f) ? resolved : String(f);
  }
  return extended;
}
