import { ExhaustiveSwitchCheck } from '@postxl/utils'

import {
  type AST,
  AST_Binary,
  AST_Compare,
  AST_Function,
  AST_Range,
  AST_Reference,
  AST_Unary,
  R1C1Parser,
  type RefSpec,
} from './parser'
import { Cell, type CellValue, type DataBlocks, type Model } from './types'

// TYPES -----------------------------------------------------------------------

export type Error = { reason: '#DIV/0!' | '#VALUE!' | '#REF!' | 'Not found!' }

const DIV0: Error = { reason: '#DIV/0!' }
const VALUE_ERR: Error = { reason: '#VALUE!' }
const REF_ERR: Error = { reason: '#REF!' }
const NOT_FOUND: Error = { reason: 'Not found!' }

export type Value = number | string | null | boolean | Error

export type Values = Map<string, Value>

type EvaluateParams = { model: Model; cache: Values; row: number; col: number }
type ExcelFunction = (node: AST_Function, params: EvaluateParams) => Value

// EVALUATION ------------------------------------------------------------------

export function evaluate(model: Model, data?: DataBlocks): Values {
  const cache: Values = new Map()

  for (const cell of model.cells) {
    evaluateCell(cell, model, cache, data)
  }

  return cache
}

function evaluateCell(cell: Cell, model: Model, cache: Values, data?: DataBlocks) {
  const row = cell.r
  const column = cell.c
  const key = `${row},${column}`

  const block = cell.binding
  const blockValue =
    block?.kind === 'block' ? evaluateBlockFunction(block.name, block.rOff, block.cOff, data) : NOT_FOUND

  if (blockValue !== NOT_FOUND && blockValue !== null) {
    cache.set(key, blockValue)
  } else if (cell.formula) {
    try {
      const parser = new R1C1Parser(cell.formula)
      const ast = parser.parse()
      const value = evaluateCellFromAST(ast, { model, cache, row, col: column })

      cache.set(key, value)
    } catch {
      const block = cell.binding
      const value =
        block?.kind === 'block' ? evaluateBlockFunction(block.name, block.rOff, block.cOff, data) : NOT_FOUND

      cache.set(key, value)
    }
  } else {
    const value = cell.value === undefined ? null : cell.value

    cache.set(key, value)
  }
}

export function evaluateCellFromAST(ast: AST, params: EvaluateParams): Value {
  return evaluateCellExpression(ast, params)
}

function evaluateCellExpression(node: AST, params: EvaluateParams): Value {
  switch (node.t) {
    case 'num':
      return node.v

    case 'str':
      return node.v

    case 'un':
      return evaluateCellExpression_Unary(node, params)

    case 'bin':
      return evaluateCellExpression_Binary(node, params)

    case 'cmp':
      return evaluateCellExpression_Comparison(node, params)

    case 'ref':
      return evaluateCellExpression_Reference(node, params)

    case 'range':
      return evaluateCellExpression_Range(node, params)

    case 'fun':
      return evaluateCellExpression_Function(node, params)

    default:
      throw new ExhaustiveSwitchCheck(node)
  }
}

function evaluateCellExpression_Unary(node: AST_Unary, params: EvaluateParams): Value {
  const value = evaluateCellExpression(node.expr, params)
  if (isErr(value)) {
    return value
  }

  const number = asNumber(value)
  if (isErr(number)) {
    return number
  }

  return node.op === '-' ? -number : +number
}

function evaluateCellExpression_Binary(node: AST_Binary, params: EvaluateParams): Value {
  const left = evaluateCellExpression(node.left, params)
  if (isErr(left)) {
    return left
  }

  const right = evaluateCellExpression(node.right, params)
  if (isErr(right)) {
    return right
  }

  const x = asNumber(left)
  if (isErr(x)) {
    return x
  }

  const y = asNumber(right)
  if (isErr(y)) {
    return y
  }

  switch (node.op) {
    case '+':
      return x + y
    case '-':
      return x - y
    case '*':
      return x * y
    case '/':
      return y === 0 ? DIV0 : x / y
    case '^':
      return Math.pow(x, y)
    default:
      return VALUE_ERR
  }
}

function evaluateCellExpression_Comparison(node: AST_Compare, params: EvaluateParams): Value {
  const left = evaluateCellExpression(node.left, params)
  if (isErr(left)) {
    return left
  }

  const right = evaluateCellExpression(node.right, params)
  if (isErr(right)) {
    return right
  }

  if (node.op === '=') {
    return left == right
  }

  if (node.op === '<>') {
    return left != right
  }

  if (left === null || right === null) {
    return VALUE_ERR
  }

  switch (node.op) {
    case '<':
      return left < right
    case '<=':
      return left <= right
    case '>':
      return left > right
    case '>=':
      return left >= right
    default:
      return VALUE_ERR
  }
}

function evaluateCellExpression_Reference(node: AST_Reference, { row, col, model, cache }: EvaluateParams): Value {
  const reference = resolveReference(model, row, col, node.ref)
  if (isErr(reference)) {
    return reference
  }

  const key = `${reference.r},${reference.c}`

  return cache.get(key) ?? NOT_FOUND
}

function evaluateCellExpression_Range(node: AST_Range, { row, col, model }: EvaluateParams): Value {
  const box = resolveRange(model, row, col, node.a, node.b)

  if (isErr(box)) {
    return box
  } else {
    // Ranges only meaningful inside SUM; return #VALUE! if used directly
    return VALUE_ERR
  }
}

const excelFunctions: Record<AST_Function['name'], ExcelFunction> = {
  SUM: evaluateCellExpression_Function_Sum,
  IF: evaluateCellExpression_If,
  IFERROR: evaluateCellExpression_IfError,
}

function evaluateCellExpression_Function(node: AST_Function, params: EvaluateParams): Value {
  const fn = excelFunctions[node.name]
  if (fn) {
    return fn(node, params)
  } else {
    return VALUE_ERR
  }
}

function evaluateCellExpression_Function_Sum(node: AST_Function, params: EvaluateParams): Value {
  const { model, cache, row, col } = params
  return node.args.reduce(
    (total, arg) => {
      if (isErr(total)) {
        return total
      }

      if (arg.t === 'range') {
        const box = resolveRange(model, row, col, arg.a, arg.b)
        if (isErr(box)) {
          return box
        }

        for (let rr = box.r0; rr <= box.r1; rr++) {
          for (let cc = box.c0; cc <= box.c1; cc++) {
            const key = `${rr},${cc}`
            const v = cache.get(key) ?? NOT_FOUND
            const n = asNumber(v)

            if (!isErr(n) && Number.isFinite(n)) {
              total += n
            }
          }
        }
      } else {
        const v = evaluateCellExpression(arg, params)
        const n = asNumber(v)

        if (!isErr(n) && Number.isFinite(n)) {
          total += n
        }
      }

      return total
    },
    0 as number | Error,
  )
}

function evaluateCellExpression_If(node: AST_Function, params: EvaluateParams): Value {
  const [cond, a, b] = node.args
  if (!cond || !a || b === undefined) {
    return VALUE_ERR
  }

  const cnd = evaluateCellExpression(cond, params)
  if (isErr(cnd)) {
    return cnd
  }

  return asBoolean(cnd) ? evaluateCellExpression(a, params) : evaluateCellExpression(b, params)
}

function evaluateCellExpression_IfError(node: AST_Function, params: EvaluateParams): Value {
  const [a, b] = node.args
  const a_eval = evaluateCellExpression(a, params)

  if (isErr(a_eval)) {
    return evaluateCellExpression(b, params)
  } else {
    return a_eval
  }
}

export function evaluateBlockFunction(
  blockName: string,
  rOff: number | undefined,
  cOff: number | undefined,
  data?: DataBlocks,
): Value {
  // Return null if no data provided or block not found
  if (!data || !(blockName in data)) {
    return NOT_FOUND
  }

  const block = data[blockName]
  const rowOffset = rOff ?? 0
  const colOffset = cOff ?? 0

  // Handle scalar values
  if (!Array.isArray(block)) {
    return block
  }

  // Handle 1D arrays
  if (!Array.isArray(block[0])) {
    const arr = block as CellValue[]
    if (colOffset < 0 || colOffset >= arr.length) {
      return REF_ERR
    }
    return arr[colOffset]
  }

  // Handle 2D arrays
  const arr2d = block as CellValue[][]
  if (rowOffset < 0 || rowOffset >= arr2d.length) {
    return REF_ERR
  }
  const row = arr2d[rowOffset]
  if (!Array.isArray(row)) {
    return VALUE_ERR
  }
  if (colOffset < 0 || colOffset >= row.length) {
    return REF_ERR
  }
  return row[colOffset]
}

// QUERIES ---------------------------------------------------------------------

function resolveReference(model: Model, row: number, column: number, ref: RefSpec) {
  const rr = ref.absR ?? row + ref.relR
  const cc = ref.absC ?? column + ref.relC

  if (rr < 0 || cc < 0 || rr >= model.rows || cc >= model.cols) {
    return REF_ERR
  } else {
    return { r: rr, c: cc }
  }
}

function resolveRange(model: Model, row: number, column: number, from: RefSpec, to: RefSpec) {
  const a = resolveReference(model, row, column, from)
  if (isErr(a)) {
    return a
  }

  const b = resolveReference(model, row, column, to)
  if (isErr(b)) {
    return b
  }

  return {
    r0: Math.min(a.r, b.r),
    c0: Math.min(a.c, b.c),
    r1: Math.max(a.r, b.r),
    c1: Math.max(a.c, b.c),
  }
}

// GUARDS ----------------------------------------------------------------------

export function isErr(value: unknown): value is Error {
  return !!value && typeof value === 'object' && 'reason' in value
}

// CONVERSIONS -----------------------------------------------------------------

function asNumber(v: Value): number | Error {
  if (isErr(v) || v === null) {
    return VALUE_ERR
  }

  const number = Number(v)

  return Number.isFinite(number) ? number : VALUE_ERR
}

function asBoolean(value: Value): boolean {
  if (isErr(value) || value === null) {
    return false
  }

  switch (typeof value) {
    case 'boolean':
      return value
    case 'number':
      return value !== 0 && !Number.isNaN(value)
    case 'string':
      return value.length > 0

    default:
      return false
  }
}
