export type CellValueReader = (viewRow: number, col: number) => unknown;

export type FormulaResult = number | 'DIV_ZERO' | null;

class DivZeroError extends Error {
  constructor() {
    super('Division by zero');
    this.name = 'DivZeroError';
  }
}

export function colLettersToIndex(letters: string): number {
  let n = 0;
  for (const ch of letters.toUpperCase()) {
    n = n * 26 + (ch.charCodeAt(0) - 64);
  }
  return n - 1;
}

export function colIndexToLetters(index: number): string {
  let n = index + 1;
  let result = '';
  while (n > 0) {
    n -= 1;
    result = String.fromCharCode(65 + (n % 26)) + result;
    n = Math.floor(n / 26);
  }
  return result;
}

function toNumber(value: unknown): number {
  if (value == null || value === '') return 0;
  const n = Number(value);
  return Number.isFinite(n) ? n : Number.NaN;
}

function tokenize(expr: string): string[] {
  const tokens: string[] = [];
  let i = 0;
  while (i < expr.length) {
    const ch = expr[i]!;
    if (ch === ' ' || ch === '\t') {
      i += 1;
      continue;
    }
    if ('+-*/()'.includes(ch)) {
      tokens.push(ch);
      i += 1;
      continue;
    }
    if (/[0-9.]/.test(ch)) {
      let num = ch;
      i += 1;
      while (i < expr.length && /[0-9.]/.test(expr[i]!)) {
        num += expr[i];
        i += 1;
      }
      tokens.push(num);
      continue;
    }
    if (/[A-Za-z]/.test(ch)) {
      let ref = ch;
      i += 1;
      while (i < expr.length && /[A-Za-z0-9]/.test(expr[i]!)) {
        ref += expr[i];
        i += 1;
      }
      tokens.push(ref);
      continue;
    }
    throw new Error(`Unexpected character: ${ch}`);
  }
  return tokens;
}

function parseFactor(
  tokens: string[],
  pos: { i: number },
  readCell: CellValueReader,
  viewRow: number,
): number {
  const token = tokens[pos.i];
  if (token === undefined) throw new Error('Unexpected end');

  if (token === '(') {
    pos.i += 1;
    const value = parseExpression(tokens, pos, readCell, viewRow);
    if (tokens[pos.i] !== ')') throw new Error('Missing )');
    pos.i += 1;
    return value;
  }

  if (token === '+') {
    pos.i += 1;
    return parseFactor(tokens, pos, readCell, viewRow);
  }

  if (token === '-') {
    pos.i += 1;
    return -parseFactor(tokens, pos, readCell, viewRow);
  }

  if (/^[0-9]+(\.[0-9]+)?$/.test(token)) {
    pos.i += 1;
    return Number(token);
  }

  const match = /^([A-Za-z]+)(\d+)$/.exec(token);
  if (match) {
    pos.i += 1;
    const col = colLettersToIndex(match[1]!);
    return toNumber(readCell(viewRow, col));
  }

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

function parseTerm(
  tokens: string[],
  pos: { i: number },
  readCell: CellValueReader,
  viewRow: number,
): number {
  let value = parseFactor(tokens, pos, readCell, viewRow);
  while (pos.i < tokens.length) {
    const op = tokens[pos.i];
    if (op === '*' || op === '/') {
      pos.i += 1;
      const rhs = parseFactor(tokens, pos, readCell, viewRow);
      if (op === '/') {
        if (rhs === 0) throw new DivZeroError();
        value /= rhs;
      } else {
        value *= rhs;
      }
    } else {
      break;
    }
  }
  return value;
}

function parseExpression(
  tokens: string[],
  pos: { i: number },
  readCell: CellValueReader,
  viewRow: number,
): number {
  let value = parseTerm(tokens, pos, readCell, viewRow);
  while (pos.i < tokens.length) {
    const op = tokens[pos.i];
    if (op === '+' || op === '-') {
      pos.i += 1;
      const rhs = parseTerm(tokens, pos, readCell, viewRow);
      value = op === '+' ? value + rhs : value - rhs;
    } else {
      break;
    }
  }
  return value;
}

export function evaluateFormula(
  formula: string,
  viewRow: number,
  readCell: CellValueReader,
): FormulaResult {
  if (!formula?.trim()) return null;
  let expr = formula.trim();
  if (expr.startsWith('=')) expr = expr.slice(1);
  if (!expr) return null;

  try {
    const tokens = tokenize(expr);
    const pos = { i: 0 };
    const result = parseExpression(tokens, pos, readCell, viewRow);
    if (pos.i < tokens.length) throw new Error('Trailing tokens');
    if (!Number.isFinite(result)) return null;
    return result;
  } catch (err) {
    if (err instanceof DivZeroError) return 'DIV_ZERO';
    return null;
  }
}

export function formatFormulaResult(value: FormulaResult): string {
  if (value === 'DIV_ZERO') return '-';
  if (value == null || Number.isNaN(value)) return '#ERROR';
  if (Number.isInteger(value)) return String(value);
  return String(Math.round(value * 100) / 100);
}
