export type AST =
  | AST_Numeric
  | AST_String
  | AST_Unary
  | AST_Binary
  | AST_Compare
  | AST_Reference
  | AST_Range
  | AST_Function
export type AST_Numeric = { t: 'num'; v: number }
export type AST_String = { t: 'str'; v: string }
export type AST_Unary = { t: 'un'; op: '+' | '-'; expr: AST }
export type AST_Binary = { t: 'bin'; op: '+' | '-' | '*' | '/' | '^'; left: AST; right: AST }
export type AST_Compare = { t: 'cmp'; op: '=' | '<>' | '<' | '<=' | '>' | '>='; left: AST; right: AST }
export type AST_Reference = { t: 'ref'; ref: RefSpec }
export type AST_Range = { t: 'range'; a: RefSpec; b: RefSpec }
export type AST_Function = { t: 'fun'; name: 'IF' | 'IFERROR' | 'SUM'; args: AST[] }

export type RefSpec = {
  relR: number
  relC: number
  absR: number | null
  absC: number | null
}

export class R1C1Parser {
  s: string
  i = 0

  constructor(input: string) {
    this.s = input.trim()
  }

  peek() {
    return this.s[this.i]
  }

  next() {
    return this.s[this.i++]
  }

  eof() {
    return this.i >= this.s.length
  }

  skipWS() {
    while (!this.eof() && /\s/.test(this.peek())) {
      this.i++
    }
  }

  // Entry: allow optional leading '='
  parse(): AST {
    this.skipWS()
    if (this.peek() === '=') {
      this.next()
    }
    this.skipWS()
    const expr = this.parseComparison()
    this.skipWS()
    return expr
  }

  // comparison: addSub (comp addSub)?
  parseComparison(): AST {
    const left = this.parseAddSub()
    this.skipWS()
    const ops = ['<=', '>=', '<>', '=', '<', '>'] as const
    for (const op of ops) {
      if (this.s.startsWith(op, this.i)) {
        this.i += op.length
        const right = this.parseAddSub()
        return { t: 'cmp', op, left, right }
      }
    }
    return left
  }

  // add/sub
  parseAddSub(): AST {
    let node = this.parseMulDiv()
    this.skipWS()
    while (true) {
      const ch = this.peek()
      if (ch === '+' || ch === '-') {
        this.next()
        const rhs = this.parseMulDiv()
        node = { t: 'bin', op: ch, left: node, right: rhs }
        this.skipWS()
      } else {
        break
      }
    }
    return node
  }

  // mul/div
  parseMulDiv(): AST {
    let node = this.parsePower()
    this.skipWS()
    while (true) {
      const ch = this.peek()
      if (ch === '*' || ch === '/') {
        this.next()
        const rhs = this.parsePower()
        node = { t: 'bin', op: ch, left: node, right: rhs }
        this.skipWS()
      } else {
        break
      }
    }
    return node
  }

  // power (right-assoc)
  parsePower(): AST {
    const left = this.parseUnary()
    this.skipWS()
    if (this.peek() === '^') {
      this.next()
      const right = this.parsePower()
      return { t: 'bin', op: '^', left, right }
    }
    return left
  }

  // unary +/-
  parseUnary(): AST {
    this.skipWS()
    const ch = this.peek()
    if (ch === '+' || ch === '-') {
      this.next()
      const expr = this.parseUnary()
      return { t: 'un', op: ch, expr }
    }
    return this.parsePrimary()
  }

  // primary: number | string | ref(:ref)? | func(...) | (expr)
  parsePrimary(): AST {
    this.skipWS()
    const ch = this.peek()

    // Parentheses
    if (ch === '(') {
      this.next()
      const e = this.parseComparison()
      this.skipWS()
      if (this.next() !== ')') {
        throw new Error("Missing ')'")
      }
      return e
    }

    // String literal "..."
    if (ch === '"') {
      this.next()
      let out = ''
      while (!this.eof() && this.peek() !== '"') {
        out += this.next()
      }
      if (this.next() !== '"') {
        throw new Error('Unclosed string')
      }
      return { t: 'str', v: out }
    }

    // Number (with optional % suffix)
    if (/[0-9.]/.test(ch)) {
      const start = this.i
      while (!this.eof() && /[0-9.]/.test(this.peek())) {
        this.next()
      }
      let num = Number.parseFloat(this.s.slice(start, this.i))
      if (this.peek() === '%') {
        this.next()
        num = num / 100
      }
      return { t: 'num', v: num }
    }

    // Function? (IF, SUM)
    if (/[A-Z]/.test(ch)) {
      // Lookahead for IF( or SUM( ; otherwise might be ref starting with R
      const ident = this.readIdent()
      if ((ident === 'IF' || ident === 'IFERROR' || ident === 'SUM') && this.peek() === '(') {
        this.next() // '('
        const args: AST[] = []
        while (true) {
          this.skipWS()
          if (this.peek() === ')') {
            this.next()
            break
          }
          // Accept comma or semicolon separators
          const arg = this.parseComparison()
          args.push(arg)
          this.skipWS()
          if (this.peek() === ',' || this.peek() === ';') {
            this.next()
            continue
          }
          if (this.peek() === ')') {
            this.next()
            break
          }
          // if not separator or ')', it's a syntax error
          throw new Error("Expected ',' or ')' in function call")
        }
        return { t: 'fun', name: ident, args }
      }
      // If ident was 'R', fall through to reference parsing (we already consumed it)
      this.i -= ident.length // rewind to start of identifier for reference parse below
    }

    // Reference or Range (R...C...[:R...C...])
    if (ch === 'R') {
      const a = this.readRef()
      this.skipWS()
      if (this.peek() === ':') {
        this.next()
        const b = this.readRef()
        return { t: 'range', a, b }
      }
      return { t: 'ref', ref: a }
    }

    throw new Error('Unexpected token at: ' + this.s.slice(this.i, this.i + 10))
  }

  readIdent(): string {
    const start = this.i
    while (!this.eof() && /[A-Z_]/.test(this.peek())) {
      this.next()
    }
    return this.s.slice(start, this.i)
  }

  readSignedIntInBrackets(): number {
    if (this.next() !== '[') {
      throw new Error("Expected '['")
    }
    const start = this.i
    if (this.peek() === '+' || this.peek() === '-') {
      this.next()
    }
    while (!this.eof() && /[0-9]/.test(this.peek())) {
      this.next()
    }
    const n = Number.parseInt(this.s.slice(start, this.i), 10)
    if (this.next() !== ']') {
      throw new Error("Expected ']'")
    }
    if (Number.isNaN(n)) {
      throw new TypeError('Bad integer')
    }
    return n
  }

  // R[dr]?C[dc]? ; also tolerate absolute RnCm
  readRef(): RefSpec {
    if (this.next() !== 'R') {
      throw new Error("Expected 'R'")
    }

    let relR = 0,
      relC = 0,
      absR: number | null = null,
      absC: number | null = null

    if (this.peek() === '[') {
      relR = this.readSignedIntInBrackets()
    } else {
      // absolute row digits or nothing (meaning current row)
      const start = this.i

      while (!this.eof() && /[0-9]/.test(this.peek())) {
        this.next()
      }

      if (this.i > start) {
        // 0-based
        absR = Number.parseInt(this.s.slice(start, this.i), 10) - 1
      }
    }

    if (this.next() !== 'C') {
      throw new Error("Expected 'C'")
    }

    if (this.peek() === '[') {
      relC = this.readSignedIntInBrackets()
    } else {
      const start = this.i

      while (!this.eof() && /[0-9]/.test(this.peek())) {
        this.next()
      }

      if (this.i > start) {
        absC = Number.parseInt(this.s.slice(start, this.i), 10) - 1
      }
    }

    return { relR, relC, absR, absC }
  }
}
