///|
priv enum PdfCalculatorToken {
  PdfCalculatorTokenOpen
  PdfCalculatorTokenClose
  PdfCalculatorTokenText(String)
}

///|
priv enum PdfCalculatorItem {
  PdfCalculatorItemOp(PdfCalculatorOp)
  PdfCalculatorItemProcedure(Array[PdfCalculatorOp])
}

///|
fn pdf_fun_calculator_number_of_token(text : String) -> PdfCalculatorOp? {
  let integer : Result[Int, Error] = try @string.parse_int(text) catch {
    err => Err(err)
  } noraise {
    value => Ok(value)
  }
  match integer {
    Ok(value) => Some(PdfCalculatorInt(value))
    Err(_) => {
      let real : Result[Double, Error] = try @string.parse_double(text) catch {
        err => Err(err)
      } noraise {
        value => Ok(value)
      }
      match real {
        Ok(value) => Some(PdfCalculatorFloat(value))
        Err(_) => None
      }
    }
  }
}

///|
fn pdf_fun_calculator_op_of_token(
  text : String,
) -> PdfCalculatorOp raise @core.PdfError {
  match text {
    "abs" => PdfCalculatorAbs
    "add" => PdfCalculatorAdd
    "and" => PdfCalculatorAnd
    "atan" => PdfCalculatorAtan
    "bitshift" => PdfCalculatorBitshift
    "true" => PdfCalculatorBool(true)
    "false" => PdfCalculatorBool(false)
    "ceiling" => PdfCalculatorCeiling
    "cos" => PdfCalculatorCos
    "copy" => PdfCalculatorCopy
    "cvi" => PdfCalculatorCvi
    "cvr" => PdfCalculatorCvr
    "sub" => PdfCalculatorSub
    "mul" => PdfCalculatorMul
    "div" => PdfCalculatorDiv
    "dup" => PdfCalculatorDup
    "exch" => PdfCalculatorExch
    "exp" => PdfCalculatorExp
    "floor" => PdfCalculatorFloor
    "idiv" => PdfCalculatorIdiv
    "eq" => PdfCalculatorEq
    "ge" => PdfCalculatorGe
    "gt" => PdfCalculatorGt
    "index" => PdfCalculatorIndex
    "le" => PdfCalculatorLe
    "lt" => PdfCalculatorLt
    "ln" => PdfCalculatorLn
    "log" => PdfCalculatorLog
    "mod" => PdfCalculatorMod
    "ne" => PdfCalculatorNe
    "neg" => PdfCalculatorNeg
    "not" => PdfCalculatorNot
    "or" => PdfCalculatorOr
    "pop" => PdfCalculatorPop
    "roll" => PdfCalculatorRoll
    "round" => PdfCalculatorRound
    "sin" => PdfCalculatorSin
    "sqrt" => PdfCalculatorSqrt
    "truncate" => PdfCalculatorTruncate
    "xor" => PdfCalculatorXor
    _ =>
      match pdf_fun_calculator_number_of_token(text) {
        Some(number) => number
        None =>
          if text.has_prefix("--") {
            match
              pdf_fun_calculator_number_of_token(
                text.unsafe_substring(start=1, end=text.length()),
              ) {
              Some(number) => number
              None => raise FunctionExpected
            }
          } else {
            raise FunctionExpected
          }
      }
  }
}

///|
fn pdf_fun_calculator_text_token(
  token : ArrayView[Byte],
) -> PdfCalculatorToken? raise @core.PdfError {
  if token.length() > 0 {
    let text = @ascii.decode(Bytes::from_array(token)) catch {
      _ => raise FunctionExpected
    }
    Some(PdfCalculatorTokenText(text))
  } else {
    None
  }
}

///|
fn pdf_fun_calculator_push_text_token(
  tokens : Array[PdfCalculatorToken],
  token : ArrayView[Byte],
) -> Unit raise @core.PdfError {
  match pdf_fun_calculator_text_token(token) {
    Some(text) => tokens.push(text)
    None => ()
  }
}

///|
fn pdf_fun_calculator_tokens(
  data : @core.PdfBytes,
) -> Array[PdfCalculatorToken] raise @core.PdfError {
  let tokens : Array[PdfCalculatorToken] = []
  let mut token : Array[Byte] = []
  let mut index = 0
  while index < data.length() {
    let byte = data[index]
    let value = byte.to_int()
    if @core.pdf_is_whitespace_byte(value) {
      pdf_fun_calculator_push_text_token(tokens, token)
      token = []
      index += 1
    } else if value == 123 {
      pdf_fun_calculator_push_text_token(tokens, token)
      token = []
      tokens.push(PdfCalculatorTokenOpen)
      index += 1
    } else if value == 125 {
      pdf_fun_calculator_push_text_token(tokens, token)
      token = []
      tokens.push(PdfCalculatorTokenClose)
      index += 1
    } else if value == 37 {
      pdf_fun_calculator_push_text_token(tokens, token)
      token = []
      index += 1
      while index < data.length() &&
            data[index].to_int() != 10 &&
            data[index].to_int() != 13 {
        index += 1
      }
    } else {
      token.push(byte)
      index += 1
    }
  }
  pdf_fun_calculator_push_text_token(tokens, token)
  tokens
}

///|
fn pdf_fun_calculator_pop_procedure(
  items : Array[PdfCalculatorItem],
) -> Array[PdfCalculatorOp] raise @core.PdfError {
  match items.pop() {
    Some(PdfCalculatorItemProcedure(operations)) => operations
    _ => raise FunctionExpected
  }
}

///|
fn pdf_fun_calculator_items_to_operations(
  items : ArrayView[PdfCalculatorItem],
) -> Array[PdfCalculatorOp] raise @core.PdfError {
  let operations : Array[PdfCalculatorOp] = []
  for item in items {
    match item {
      PdfCalculatorItemOp(operation) => operations.push(operation)
      PdfCalculatorItemProcedure(_) => raise FunctionExpected
    }
  }
  operations
}

///|
fn pdf_fun_calculator_push_operator(
  items : Array[PdfCalculatorItem],
  text : String,
) -> Unit raise @core.PdfError {
  match text {
    "if" => {
      let then_branch = pdf_fun_calculator_pop_procedure(items)
      items.push(PdfCalculatorItemOp(PdfCalculatorIf(then_branch)))
    }
    "ifelse" => {
      let else_branch = pdf_fun_calculator_pop_procedure(items)
      let then_branch = pdf_fun_calculator_pop_procedure(items)
      items.push(
        PdfCalculatorItemOp(PdfCalculatorIfElse(then_branch, else_branch)),
      )
    }
    _ => items.push(PdfCalculatorItemOp(pdf_fun_calculator_op_of_token(text)))
  }
}

///|
fn pdf_fun_calculator_parse_items(
  tokens : ArrayView[PdfCalculatorToken],
  start : Int,
  stop_at_close~ : Bool,
) -> (Array[PdfCalculatorItem], Int) raise @core.PdfError {
  let items : Array[PdfCalculatorItem] = []
  let mut index = start
  let mut closed = false
  while index < tokens.length() && !closed {
    match tokens[index] {
      PdfCalculatorTokenOpen => {
        let (operations, next_index) = pdf_fun_calculator_parse_operations(
          tokens,
          index + 1,
          stop_at_close=true,
        )
        items.push(PdfCalculatorItemProcedure(operations))
        index = next_index
      }
      PdfCalculatorTokenClose =>
        if stop_at_close {
          closed = true
          index += 1
        } else {
          raise FunctionExpected
        }
      PdfCalculatorTokenText(text) => {
        pdf_fun_calculator_push_operator(items, text)
        index += 1
      }
    }
  }
  if stop_at_close && !closed {
    raise FunctionExpected
  }
  (items, index)
}

///|
fn pdf_fun_calculator_parse_operations(
  tokens : ArrayView[PdfCalculatorToken],
  start : Int,
  stop_at_close~ : Bool,
) -> (Array[PdfCalculatorOp], Int) raise @core.PdfError {
  let (items, next_index) = pdf_fun_calculator_parse_items(
    tokens,
    start,
    stop_at_close~,
  )
  (pdf_fun_calculator_items_to_operations(items), next_index)
}

///|
fn pdf_fun_parse_calculator(
  data : @core.PdfBytes,
) -> PdfCalculatorFunction raise @core.PdfError {
  let tokens = pdf_fun_calculator_tokens(data)
  let (operations, next_index) = match tokens.get(0) {
    Some(PdfCalculatorTokenOpen) =>
      pdf_fun_calculator_parse_operations(tokens, 1, stop_at_close=true)
    _ => pdf_fun_calculator_parse_operations(tokens, 0, stop_at_close=false)
  }
  if next_index != tokens.length() {
    raise FunctionExpected
  }
  { operations, }
}