///|
fn digit_value(text : String) -> Int? {
  if text.length() != 1 {
    None
  } else {
    let code = text[0]
    if code >= '0' && code <= '9' {
      Some(code.to_int() - ('0' : UInt16).to_int())
    } else if code >= 'a' && code <= 'f' {
      Some(code.to_int() - ('a' : UInt16).to_int() + 10)
    } else if code >= 'A' && code <= 'F' {
      Some(code.to_int() - ('A' : UInt16).to_int() + 10)
    } else {
      None
    }
  }
}

///|
fn char_code_from_token(token : Token) -> Int raise ParseFailure {
  if token.text == "EOF" {
    raise InvalidArgument(message="\\char` missing argument", loc=token.loc)
  } else if token.text.length() > 1 && token.text[0] == '\\' {
    token.text[1].to_int()
  } else {
    token.text[0].to_int()
  }
}

///|
fn parse_char_number(
  context : MacroExpander,
  first : Token,
  base : Int,
) -> Int raise ParseFailure {
  guard digit_value(first.text) is Some(value) && value < base else {
    raise InvalidArgument(
      message="Invalid base-\{base} digit \{first.text}",
      loc=first.loc,
    )
  }
  let first_digit = value
  for number = first_digit {
    let token = context.future()
    match digit_value(token.text) {
      Some(digit) if digit < base => {
        let _ = context.pop_token()
        let next = if number > (2147483647 - digit) / base {
          2147483647
        } else {
          number * base + digit
        }
        continue next
      }
      _ => break number
    }
  }
}

///|
fn char_macro(context : MacroExpander) -> MacroReplacement raise ParseFailure {
  let first = context.pop_token()
  let number = if first.text == "`" {
    char_code_from_token(context.pop_token())
  } else {
    let (base, digit) = if first.text == "'" {
      (8, context.pop_token())
    } else if first.text == "\"" {
      (16, context.pop_token())
    } else {
      (10, first)
    }
    parse_char_number(context, digit, base)
  }
  ReplacementText("\\@char{\{number}}")
}

///|
fn parse_argument_count(text : String) -> Int? {
  let mut number = 0
  let mut saw_digit = false
  let mut trailing_space = false
  for code in text {
    if code >= '0' && code <= '9' && !trailing_space {
      saw_digit = true
      let digit = code.to_int() - ('0' : Char).to_int()
      number = if number > (2147483647 - digit) / 10 {
        2147483647
      } else {
        number * 10 + digit
      }
    } else if code == ' ' || code == '\t' || code == '\n' || code == '\r' {
      if saw_digit {
        trailing_space = true
      }
    } else {
      return None
    }
  }
  if saw_digit {
    Some(number)
  } else {
    None
  }
}

///|
fn define_command_macro(
  context : MacroExpander,
  exists_ok : Bool,
  nonexists_ok : Bool,
  skip_if_exists : Bool,
) -> MacroReplacement raise ParseFailure {
  let name_arg = context.consume_arg(None).tokens
  guard name_arg.length() == 1 else {
    raise InvalidArgument(
      message="\\newcommand's first argument must be a macro name",
      loc=None,
    )
  }
  let name = name_arg[0].text
  let exists = context.is_defined(name)
  guard !exists || exists_ok else {
    raise InvalidArgument(
      message="\\newcommand{\{name}} attempting to redefine \{name}; use \\renewcommand",
      loc=None,
    )
  }
  guard exists || nonexists_ok else {
    raise InvalidArgument(
      message="\\renewcommand{\{name}} when command \{name} does not yet exist; use \\newcommand",
      loc=None,
    )
  }
  let mut body = context.consume_arg(None).tokens
  let mut num_args = 0
  if body.length() == 1 && body[0].text == "[" {
    let builder = StringBuilder()
    for ;; {
      let token = context.expand_next_token()
      if token.text == "]" || token.text == "EOF" {
        break
      }
      builder.write_string(token.text)
    }
    let count_text = builder.to_string()
    guard parse_argument_count(count_text) is Some(value) else {
      raise InvalidArgument(
        message="Invalid number of arguments: \{count_text}",
        loc=None,
      )
    }
    num_args = value
    body = context.consume_arg(None).tokens
  }
  if !(exists && skip_if_exists) {
    context.macros.set(
      name,
      Some(MacroDefinition::expansion(MacroExpansion::make(body, num_args~))),
    )
  }
  ReplacementText("")
}

///|
fn new_command_macro(
  context : MacroExpander,
) -> MacroReplacement raise ParseFailure {
  define_command_macro(context, false, true, false)
}

///|
fn renew_command_macro(
  context : MacroExpander,
) -> MacroReplacement raise ParseFailure {
  define_command_macro(context, true, false, false)
}

///|
fn provide_command_macro(
  context : MacroExpander,
) -> MacroReplacement raise ParseFailure {
  define_command_macro(context, true, true, true)
}