///|
priv suberror SuffixError {
  SuffixError(String)
}

///|
priv struct SuffixFormat {
  prefix : String
  suffix : String
  conversion : UInt16
  width : Int32
  precision : Int32?
  left : Bool
  zero : Bool
  alternate : Bool
}

///|
fn suffix_format(value : String) -> SuffixFormat raise {
  let before = StringBuilder()
  let after = StringBuilder()
  let mut found = None
  let mut index = 0
  while index < value.length() {
    let literal = if found is None { before } else { after }
    if value[index] != '%' {
      literal.write_char(value[index].to_int().unsafe_to_char())
      index += 1
      continue
    }
    index += 1
    if index < value.length() && value[index] == '%' {
      literal.write_char('%')
      index += 1
      continue
    }
    guard found is None else {
      raise SuffixError("too many % conversion specifications in suffix")
    }
    let mut left = false
    let mut zero = false
    let mut alternate = false
    let mut thousands = false
    while index < value.length() && value[index] is ('-' | '0' | '#' | '\'') {
      match value[index] {
        '-' => left = true
        '0' => zero = true
        '#' => alternate = true
        _ => thousands = true
      }
      index += 1
    }
    let start = index
    while index < value.length() && value[index] is ('0'..<='9') {
      index += 1
    }
    let width = if index == start {
      0
    } else {
      @string.parse_int(value[start:index])
    }
    let precision = if index < value.length() && value[index] == '.' {
      index += 1
      let start = index
      while index < value.length() && value[index] is ('0'..<='9') {
        index += 1
      }
      Some(
        if start == index {
          0
        } else {
          @string.parse_int(value[start:index])
        },
      )
    } else {
      None
    }
    guard index < value.length() else {
      raise SuffixError("missing conversion specifier in suffix")
    }
    let conversion = value[index]
    guard conversion is ('d' | 'i' | 'u' | 'o' | 'x' | 'X') else {
      let octal = conversion.to_int().to_string(radix=8)
      let display = if conversion >= ' ' && conversion <= '~' {
        conversion.to_int().unsafe_to_char().to_string()
      } else {
        "\\" + "0".repeat(@cmp.maximum(0, 3 - octal.length())) + octal
      }
      raise SuffixError("invalid conversion specifier in suffix: " + display)
    }
    let decimal = conversion is ('d' | 'i' | 'u')
    if (decimal && alternate) || (!decimal && thousands) {
      let flag = if decimal { "#" } else { "'" }
      let conversion : UInt16 = if conversion == 'u' { 'd' } else { conversion }
      raise SuffixError(
        "invalid flags in conversion specification: %" +
        flag +
        conversion.to_int().unsafe_to_char().to_string(),
      )
    }
    found = Some((conversion, width, precision, left, zero, alternate))
    index += 1
  }
  guard found is Some((conversion, width, precision, left, zero, alternate)) else {
    raise SuffixError("missing % conversion specification in suffix")
  }
  {
    prefix: before.to_string(),
    suffix: after.to_string(),
    conversion,
    width,
    precision,
    left,
    zero,
    alternate,
  }
}

///|
fn SuffixFormat::render(self : SuffixFormat, number : Int32) -> String {
  let radix = match self.conversion {
    'o' => 8
    'x' | 'X' => 16
    _ => 10
  }
  let mut digits = if number == 0 && self.precision == Some(0) {
    ""
  } else {
    number.to_string(radix~)
  }
  if self.conversion == 'X' {
    digits = digits.to_upper()
  }
  let precision = self.precision.unwrap_or(1)
  digits = "0".repeat(@cmp.maximum(0, precision - digits.length())) + digits
  let mut marker = ""
  if self.alternate {
    if radix == 8 && !digits.has_prefix("0") {
      marker = "0"
    } else if radix == 16 && number != 0 {
      marker = if self.conversion == 'X' { "0X" } else { "0x" }
    }
  }
  let padding = @cmp.maximum(0, self.width - marker.length() - digits.length())
  let value = if self.left {
    marker + digits + " ".repeat(padding)
  } else if self.zero && self.precision is None {
    marker + "0".repeat(padding) + digits
  } else {
    " ".repeat(padding) + marker + digits
  }
  self.prefix + value + self.suffix
}