// DiceParser.js

function roll(sides) {
  return Math.floor(Math.random() * sides) + 1;
}

function tokenize(expr) {
  const re = /(\d*d\d+(?:adv|dis|kh\d+|kl\d+)?|\d+|[+\-*\/()])/gi;
  const tokens = [];
  let m;
  while ((m = re.exec(expr)) !== null) tokens.push(m[0].toLowerCase());
  return tokens;
}

function parseDiceToken(tok) {
  const m = tok.match(/^(\d*)d(\d+)(adv|dis|kh(\d+)|kl(\d+))?$/i);
  if (!m) return null;
  const count = parseInt(m[1] || '1');
  const sides = parseInt(m[2]);
  const mod = (m[3] || '').toLowerCase();
  if (count < 1 || count > 1000 || sides < 2 || sides > 10000)
    throw new Error(`Invalid dice: ${tok}`);
  return { count, sides, mod };
}

export function parse(expr) {
  const tokens = tokenize(expr);
  if (!tokens.length) throw new Error('Empty expression');
  let pos = 0;

  const peek = () => tokens[pos];
  const consume = () => tokens[pos++];

  function parseExpr() { return parseAddSub(); }

  function parseAddSub() {
    let left = parseMulDiv();
    while (peek() === '+' || peek() === '-') {
      const op = consume();
      const right = parseMulDiv();
      left = {
        value: op === '+' ? left.value + right.value : left.value - right.value,
        steps: [...left.steps, { type: 'op', op }, ...right.steps],
      };
    }
    return left;
  }

  function parseMulDiv() {
    let left = parseUnary();
    while (peek() === '*' || peek() === '/') {
      const op = consume();
      const right = parseUnary();
      if (op === '/' && right.value === 0) throw new Error('Division by zero');
      left = {
        value: op === '*' ? left.value * right.value : Math.floor(left.value / right.value),
        steps: [...left.steps, { type: 'op', op }, ...right.steps],
      };
    }
    return left;
  }

  function parseUnary() {
    if (peek() === '-') {
      consume();
      const r = parsePrimary();
      return { value: -r.value, steps: [{ type: 'op', op: '-' }, ...r.steps] };
    }
    return parsePrimary();
  }

  function parsePrimary() {
    const tok = peek();
    if (!tok) throw new Error('Unexpected end of expression');

    if (tok === '(') {
      consume();
      const inner = parseExpr();
      if (peek() !== ')') throw new Error('Missing closing )');
      consume();
      return {
        value: inner.value,
        steps: [{ type: 'op', op: '(' }, ...inner.steps, { type: 'op', op: ')' }],
      };
    }

    const diceInfo = parseDiceToken(tok);
    if (diceInfo) {
      consume();
      return rollDice(diceInfo);
    }

    if (/^\d+$/.test(tok)) {
      consume();
      const v = parseInt(tok);
      return { value: v, steps: [{ type: 'const', value: v }] };
    }

    throw new Error(`Unexpected token: ${tok}`);
  }

  function rollDice({ count, sides, mod }) {
    let rawRolls, kept;

    if (mod === 'adv') {
      rawRolls = [roll(sides), roll(sides)];
      kept = [Math.max(...rawRolls)];
    } else if (mod === 'dis') {
      rawRolls = [roll(sides), roll(sides)];
      kept = [Math.min(...rawRolls)];
    } else if (mod.startsWith('kh')) {
      const k = parseInt(mod.slice(2));
      rawRolls = Array.from({ length: count }, () => roll(sides));
      kept = [...rawRolls].sort((a, b) => b - a).slice(0, k);
    } else if (mod.startsWith('kl')) {
      const k = parseInt(mod.slice(2));
      rawRolls = Array.from({ length: count }, () => roll(sides));
      kept = [...rawRolls].sort((a, b) => a - b).slice(0, k);
    } else {
      rawRolls = Array.from({ length: count }, () => roll(sides));
      kept = rawRolls.slice();
    }

    const entry = { sides, rawRolls, kept, mod, value: kept.reduce((a, b) => a + b, 0) };
    return {
      value: entry.value,
      steps: [{ type: 'dice', entry }],
    };
  }

  const result = parseExpr();
  if (pos < tokens.length) throw new Error(`Unexpected token: ${tokens[pos]}`);
  return { total: result.value, steps: result.steps };
}