// Rôle : analyse et évalue les formules limitées de l'outil tableau.
const CELL_REFERENCE_RE = /^[A-Z]+[1-9]\d*$/;

export function columnIndexToName(index) {
  let value = Math.max(0, Number(index) || 0) + 1;
  let name = "";
  while (value > 0) {
    const remainder = (value - 1) % 26;
    name = String.fromCharCode(65 + remainder) + name;
    value = Math.floor((value - 1) / 26);
  }
  return name;
}

export function cellAddress(rowIndex, columnIndex) {
  return `${columnIndexToName(columnIndex)}${rowIndex + 1}`;
}

export function parseCellAddress(address) {
  const match = String(address || "").toUpperCase().match(/^([A-Z]+)([1-9]\d*)$/);
  if (!match) return null;
  const columnName = match[1];
  let columnIndex = 0;
  for (const char of columnName) {
    columnIndex = columnIndex * 26 + char.charCodeAt(0) - 64;
  }
  return { rowIndex: Number(match[2]) - 1, columnIndex: columnIndex - 1 };
}

export function isCellInBounds(address, rows, columns) {
  const parsed = parseCellAddress(address);
  return Boolean(parsed && parsed.rowIndex >= 0 && parsed.rowIndex < rows && parsed.columnIndex >= 0 && parsed.columnIndex < columns);
}

function parsePlainNumber(value) {
  const normalized = String(value || "").trim().replace(",", ".");
  if (!normalized || !/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(normalized)) return null;
  const number = Number(normalized);
  return Number.isFinite(number) ? number : null;
}

function formatNumber(value) {
  if (!Number.isFinite(value)) return "";
  return Number.parseFloat(value.toFixed(8)).toString();
}

function tokenize(expression) {
  const tokens = [];
  let index = 0;

  while (index < expression.length) {
    const char = expression[index];
    if (/\s/.test(char)) {
      index += 1;
      continue;
    }
    if ("+-*/()".includes(char)) {
      tokens.push({ type: char, value: char });
      index += 1;
      continue;
    }

    const numberMatch = expression.slice(index).match(/^(?:\d+(?:[\.,]\d*)?|[\.,]\d+)/);
    if (numberMatch) {
      const value = Number(numberMatch[0].replace(",", "."));
      if (!Number.isFinite(value)) throw new Error("invalid");
      tokens.push({ type: "number", value });
      index += numberMatch[0].length;
      continue;
    }

    const referenceMatch = expression.slice(index).toUpperCase().match(/^[A-Z]+[1-9]\d*/);
    if (referenceMatch) {
      tokens.push({ type: "reference", value: referenceMatch[0] });
      index += referenceMatch[0].length;
      continue;
    }

    throw new Error("invalid");
  }

  return tokens;
}

function createParser(tokens, resolveReference) {
  let index = 0;

  function current() {
    return tokens[index];
  }

  function consume(type) {
    if (current()?.type !== type) return false;
    index += 1;
    return true;
  }

  function parsePrimary() {
    const token = current();
    if (!token) throw new Error("invalid");
    if (consume("number")) return token.value;
    if (consume("reference")) return resolveReference(token.value);
    if (consume("(")) {
      const value = parseExpression();
      if (!consume(")")) throw new Error("invalid");
      return value;
    }
    throw new Error("invalid");
  }

  function parseUnary() {
    if (consume("+")) return parseUnary();
    if (consume("-")) return -parseUnary();
    return parsePrimary();
  }

  function parseTerm() {
    let value = parseUnary();
    while (current()?.type === "*" || current()?.type === "/") {
      const operator = current().type;
      index += 1;
      const right = parseUnary();
      if (operator === "*") {
        value *= right;
      } else {
        if (right === 0) throw new Error("division");
        value /= right;
      }
    }
    return value;
  }

  function parseExpression() {
    let value = parseTerm();
    while (current()?.type === "+" || current()?.type === "-") {
      const operator = current().type;
      index += 1;
      const right = parseTerm();
      value = operator === "+" ? value + right : value - right;
    }
    return value;
  }

  function parse() {
    const value = parseExpression();
    if (index < tokens.length) throw new Error("invalid");
    return value;
  }

  return { parse };
}

export function evaluateFormula(expression, resolveReference) {
  try {
    const tokens = tokenize(String(expression || ""));
    if (!tokens.length) return { value: null, error: "invalid" };
    const value = createParser(tokens, resolveReference).parse();
    return Number.isFinite(value) ? { value, error: "" } : { value: null, error: "invalid" };
  } catch (error) {
    return { value: null, error: error.message === "division" ? "division" : error.message || "invalid" };
  }
}

export function evaluateTableCell(address, cells, dimensions, stack = []) {
  const raw = String(cells?.[address] || "").trim();
  if (!raw) return { value: null, display: "", error: "empty" };

  if (stack.includes(address)) return { value: null, display: "#CYCLE", error: "cycle" };

  if (!raw.startsWith("=")) {
    const value = parsePlainNumber(raw);
    return value == null
      ? { value: null, display: raw, error: "" }
      : { value, display: formatNumber(value), error: "" };
  }

  const result = evaluateFormula(raw.slice(1), (reference) => {
    if (!CELL_REFERENCE_RE.test(reference) || !isCellInBounds(reference, dimensions.rows, dimensions.columns)) throw new Error("reference");
    const value = evaluateTableCell(reference, cells, dimensions, [...stack, address]);
    if (value.error || value.value == null) throw new Error(value.error === "cycle" ? "cycle" : "reference");
    return value.value;
  });

  if (result.error) {
    const label = result.error === "cycle" ? "#CYCLE" : result.error === "division" ? "#DIV/0" : "#ERREUR";
    return { value: null, display: label, error: result.error };
  }

  return { value: result.value, display: formatNumber(result.value), error: "" };
}

export function evaluateTable(cells, dimensions) {
  const evaluated = {};
  for (let rowIndex = 0; rowIndex < dimensions.rows; rowIndex += 1) {
    for (let columnIndex = 0; columnIndex < dimensions.columns; columnIndex += 1) {
      const address = cellAddress(rowIndex, columnIndex);
      evaluated[address] = evaluateTableCell(address, cells, dimensions);
    }
  }
  return evaluated;
}
