// Python's format-specification mini-language: parse the spec, then lay the
// number out in six parts.

///|
/// A parsed format specifier.
priv struct Spec {
  mut fill : Char
  mut align : Char
  mut sign : Char
  mut neg0 : Bool
  mut alt : Bool
  mut zero : Bool
  mut width : Int32
  mut group : Char
  mut precision : Int32
  mut ty : Char
  mut ty_at : Int32
}

///|
/// Whether `c` is one of the four alignment characters.
fn is_align(c : Char) -> Bool {
  c == '<' || c == '>' || c == '^' || c == '='
}

///|
/// Whether `c` is a presentation type this library knows.
fn is_type(c : Char) -> Bool {
  match c {
    'b'
    | 'c'
    | 'd'
    | 'e'
    | 'E'
    | 'f'
    | 'F'
    | 'g'
    | 'G'
    | 'n'
    | 'o'
    | 'x'
    | 'X'
    | '%' => true
    _ => false
  }
}

///|
/// Parse a format specifier, in CPython's order and with CPython's refusals.
fn parse_spec(text : StringView) -> Spec raise Refused {
  let s = text.to_owned()
  let n = s.length()
  let mut i = 0
  let spec : Spec = {
    fill: ' ',
    align: '>',
    sign: '\u{0}',
    neg0: false,
    alt: false,
    zero: false,
    width: -1,
    group: '\u{0}',
    precision: -1,
    ty: '\u{0}',
    ty_at: 0,
  }
  let mut align_given = false
  let mut fill_given = false
  if n >= 2 && is_align(s.get_char(1).unwrap()) {
    spec.fill = s.get_char(0).unwrap()
    spec.align = s.get_char(1).unwrap()
    fill_given = true
    align_given = true
    i = 2
    if spec.fill == '{' || spec.fill == '}' {
      malformed(0, "invalid fill character")
    }
  } else if n >= 1 && is_align(s.get_char(0).unwrap()) {
    spec.align = s.get_char(0).unwrap()
    align_given = true
    i = 1
  }
  if i < n {
    match s.get_char(i).unwrap() {
      '+' | '-' | ' ' => {
        spec.sign = s.get_char(i).unwrap()
        i += 1
      }
      _ => ()
    }
  }
  if i < n && s.get_char(i) == Some('z') {
    spec.neg0 = true
    i += 1
  }
  if i < n && s.get_char(i) == Some('#') {
    spec.alt = true
    i += 1
  }
  if !fill_given && i < n && s.get_char(i) == Some('0') {
    spec.fill = '0'
    if !align_given {
      spec.align = '='
    }
    i += 1
  }
  let mut width = 0
  let mut width_seen = false
  while i < n && is_digit(s.get_char(i).unwrap()) {
    width = width * 10 + s.get_char(i).unwrap().to_int() - '0'.to_int()
    width_seen = true
    i += 1
  }
  if width_seen {
    spec.width = width
  }
  spec.zero = spec.fill == '0' && spec.align == '='
  if i < n && s.get_char(i) == Some(',') {
    spec.group = ','
    i += 1
  }
  if i < n && s.get_char(i) == Some('_') {
    if spec.group != '\u{0}' {
      malformed(i, "cannot specify both ',' and '_'")
    }
    spec.group = '_'
    i += 1
  }
  if i < n && s.get_char(i) == Some('.') {
    let dot = i
    i += 1
    let mut precision = 0
    let mut seen = false
    while i < n && is_digit(s.get_char(i).unwrap()) {
      precision = precision * 10 +
        s.get_char(i).unwrap().to_int() -
        '0'.to_int()
      seen = true
      i += 1
    }
    if !seen {
      malformed(dot, "format specifier missing precision")
    }
    spec.precision = precision
  }
  if n - i > 1 {
    malformed(i, "invalid format specifier")
  }
  if n - i == 1 {
    let c = s.get_char(i).unwrap()
    if !is_type(c) {
      malformed(i, "unknown presentation type")
    }
    spec.ty = c
    spec.ty_at = i
    i += 1
  }
  let _ = i
  spec
}

///|
/// The width a digit string occupies once `sep` is inserted every `size`.
fn grouped_len(digits : Int32, size : Int32) -> Int32 {
  if digits <= 0 || size <= 0 {
    digits
  } else {
    digits + (digits - 1) / size
  }
}

///|
/// Split text into its sign and the rest.
fn split_sign(text : String) -> (Char, String) {
  if text.length() > 0 {
    match text.get_char(0).unwrap() {
      '-' => return ('-', slice(text, 1, text.length()))
      '+' => return ('+', slice(text, 1, text.length()))
      _ => ()
    }
  }
  ('\u{0}', text)
}

///|
/// Lay a formatted number out in a field: padding, sign, prefix, digits,
/// point and fraction, exponent.
///
/// Zero padding goes inside the sign and participates in grouping, which is why
/// this is not a call to `pad`.
fn layout_number(
  sign : Char,
  prefix : String,
  int_digits : String,
  tail : String,
  spec : Spec,
  sep : Char,
  size : Int32,
) -> String {
  let sign_text = if sign == '\u{0}' { "" } else { sign.to_string() }
  let fixed = sign_text.length() + prefix.length() + tail.length()
  let mut digits = int_digits
  let mut grouped = if sep == '\u{0}' || digits == "" {
    digits
  } else {
    group_digits(digits, sep, size)
  }
  if spec.zero && spec.width > 0 && digits != "" {
    let target = spec.width - fixed
    if grouped_len(digits.length(), if sep == '\u{0}' { 1 } else { size }) <
      target &&
      target > digits.length() {
      // Grow the digit run until the grouped width reaches the field.
      let wanted = target * size / (size + 1) + 1
      let mut estimate = wanted
      if estimate < digits.length() {
        estimate = digits.length()
      }
      let mut d = estimate
      while grouped_len(d, size) < target {
        d += 1
      }
      while d > digits.length() && grouped_len(d - 1, size) >= target {
        d -= 1
      }
      digits = repeat_char('0', d - digits.length()) + digits
      grouped = if sep == '\u{0}' {
        digits
      } else {
        group_digits(digits, sep, size)
      }
    }
  }
  let content = prefix + grouped + tail
  let padding = spec.width - sign_text.length() - content.length()
  if padding <= 0 {
    return sign_text + content
  }
  match spec.align {
    '<' => sign_text + content + repeat_char(spec.fill, padding)
    '^' => {
      let left = padding / 2
      repeat_char(spec.fill, left) +
      sign_text +
      content +
      repeat_char(spec.fill, padding - left)
    }
    // `=` pads inside the sign and the base prefix, not before them.
    '=' => sign_text + prefix + repeat_char(spec.fill, padding) + grouped + tail
    _ => repeat_char(spec.fill, padding) + sign_text + content
  }
}

///|
/// Fill an already-composed string to `spec.width`.
fn layout_text(text : String, spec : Spec) -> String {
  let padding = spec.width - text.length()
  if padding <= 0 {
    return text
  }
  match spec.align {
    '<' => text + repeat_char(spec.fill, padding)
    '^' => {
      let left = padding / 2
      repeat_char(spec.fill, left) +
      text +
      repeat_char(spec.fill, padding - left)
    }
    _ => repeat_char(spec.fill, padding) + text
  }
}

///|
/// The `sep` and group size for a presentation type: four for binary, octal and
/// hexadecimal digits, three everywhere else.
fn grouping_for(spec : Spec, ty : Char) -> (Char, Int32) {
  match spec.group {
    ',' => (',', 3)
    '_' =>
      match ty {
        'b' | 'o' | 'x' | 'X' => ('_', 4)
        _ => ('_', 3)
      }
    _ => ('\u{0}', 3)
  }
}

///|
/// The digits of `n` in `base`, uppercase when `upper`.
fn digits_in_base(n : @bigint.BigInt, base : Int32, upper : Bool) -> String {
  let text = n.to_string(radix=base)
  if upper {
    let sb = StringBuilder()
    let mut i = 0
    while i < text.length() {
      sb.write_char(text.get_char(i).unwrap().to_ascii_uppercase())
      i += 1
    }
    sb.to_string()
  } else {
    text
  }
}

///|
/// Format an integer, including the float types Python allows on integers by
/// converting them.
pub fn format_int(
  n : @bigint.BigInt,
  spec_text : StringView,
) -> String raise Refused {
  let spec = parse_spec(spec_text)
  let ty = if spec.ty == '\u{0}' { 'd' } else { spec.ty }
  match ty {
    'e' | 'E' | 'f' | 'F' | 'g' | 'G' | '%' => {
      let value = big_to_double(n)
      if value.is_inf() {
        raise Overflow(reason="int too large to convert to float")
      }
      format_float_with(value, spec, false)
    }
    'c' => {
      if spec.sign != '\u{0}' {
        malformed(spec.ty_at, "sign is not allowed with 'c'")
      }
      if spec.alt {
        malformed(spec.ty_at, "alternate form is not allowed with 'c'")
      }
      if spec.precision >= 0 {
        malformed(spec.ty_at, "precision is not allowed with 'c'")
      }
      if n < 0N || n > 0x10FFFFN {
        raise Overflow(reason="character argument not in range(0x110000)")
      }
      let code = n.to_int()
      let body = match code.to_char() {
        Some(c) => c.to_string()
        None =>
          raise Overflow(reason="character argument not in range(0x110000)")
      }
      // A width counts characters, and an astral character is still one of
      // them even though it occupies two UTF-16 units.
      let padding = spec.width - 1
      if padding <= 0 {
        body
      } else {
        match spec.align {
          '<' => body + repeat_char(spec.fill, padding)
          '^' => {
            let left = padding / 2
            repeat_char(spec.fill, left) +
            body +
            repeat_char(spec.fill, padding - left)
          }
          _ => repeat_char(spec.fill, padding) + body
        }
      }
    }
    _ => {
      if spec.precision >= 0 {
        malformed(spec.ty_at, "precision is not allowed for integers")
      }
      if spec.group == ',' && (ty == 'b' || ty == 'o' || ty == 'x' || ty == 'X') {
        malformed(spec.ty_at, "cannot specify ',' with this presentation type")
      }
      if spec.group != '\u{0}' && ty == 'n' {
        malformed(spec.ty_at, "cannot specify a grouping separator with 'n'")
      }
      if spec.neg0 {
        malformed(
          spec.ty_at,
          "negative-zero coercion is not allowed for integers",
        )
      }
      let negative = n < 0N
      let magnitude = if negative { -n } else { n }
      let (prefix, body) = match ty {
        'b' =>
          (
            if spec.alt {
              "0b"
            } else {
              ""
            },
            digits_in_base(magnitude, 2, false),
          )
        'o' =>
          (
            if spec.alt {
              "0o"
            } else {
              ""
            },
            digits_in_base(magnitude, 8, false),
          )
        'x' =>
          (
            if spec.alt {
              "0x"
            } else {
              ""
            },
            digits_in_base(magnitude, 16, false),
          )
        'X' =>
          (
            if spec.alt {
              "0X"
            } else {
              ""
            },
            digits_in_base(magnitude, 16, true),
          )
        _ => ("", magnitude.to_string())
      }
      let sign = if negative {
        '-'
      } else {
        match spec.sign {
          '+' => '+'
          ' ' => ' '
          _ => '\u{0}'
        }
      }
      let (sep, size) = grouping_for(spec, ty)
      layout_number(sign, prefix, body, "", spec, sep, size)
    }
  }
}

///|
/// The body of a float in `ty`, without any field padding.
fn float_component(
  d : Double,
  ty : Char,
  precision : Int32,
  alt : Bool,
  neg0 : Bool,
  always_sign : Bool,
) -> String {
  let mut code = 'r'
  let mut digits = 0
  let mut add_dot0 = false
  let mut upper = false
  let mut value = d
  let mut percent = false
  let given = if precision < 0 { 6 } else { precision }
  match ty {
    'r' => {
      code = 'r'
      add_dot0 = false
    }
    '\u{0}' =>
      if precision < 0 {
        code = 'r'
        add_dot0 = true
      } else {
        // A precision without a type keeps the "at least one digit after the
        // point" rule, and reads as `g` for everything else. `g` with a
        // precision of zero means one significant digit.
        code = 'g'
        digits = if given < 1 { 1 } else { given }
        add_dot0 = true
      }
    'e' => {
      code = 'e'
      digits = given + 1
    }
    'E' => {
      code = 'e'
      digits = given + 1
      upper = true
    }
    'f' => {
      code = 'f'
      digits = given
    }
    'F' => {
      code = 'f'
      digits = given
      upper = true
    }
    'g' => {
      code = 'g'
      digits = if given < 1 { 1 } else { given }
    }
    'G' => {
      code = 'g'
      digits = if given < 1 { 1 } else { given }
      upper = true
    }
    'n' => {
      code = 'g'
      digits = if given < 1 { 1 } else { given }
    }
    _ => {
      code = 'f'
      digits = given
      value = d * 100.0
      percent = true
    }
  }
  let text = float_short(
    value, code, digits, always_sign, add_dot0, alt, neg0, upper,
  )
  if percent {
    text + "%"
  } else {
    text
  }
}

///|
/// Format a double with an already parsed spec.
fn format_float_with(
  d : Double,
  spec : Spec,
  always_sign : Bool,
) -> String raise Refused {
  let ty = spec.ty
  if ty == 'b' || ty == 'c' || ty == 'd' || ty == 'o' || ty == 'x' || ty == 'X' {
    malformed(spec.ty_at, "unknown format code for a float")
  }
  if ty == 'n' && spec.group != '\u{0}' {
    malformed(spec.ty_at, "cannot specify a grouping separator with 'n'")
  }
  let text = float_component(
    d,
    ty,
    spec.precision,
    spec.alt,
    spec.neg0,
    always_sign,
  )
  let (sign_char, body) = split_sign(text)
  let mut sign = sign_char
  if sign == '\u{0}' && !always_sign {
    match spec.sign {
      '+' => sign = '+'
      ' ' => sign = ' '
      _ => ()
    }
  }
  let (sep, size) = grouping_for(spec, if ty == '\u{0}' { 'g' } else { ty })
  let mut int_len = 0
  while int_len < body.length() && is_digit(body.get_char(int_len).unwrap()) {
    int_len += 1
  }
  layout_number(
    sign,
    "",
    slice(body, 0, int_len),
    slice(body, int_len, body.length()),
    spec,
    sep,
    size,
  )
}

///|
/// Format a double the way Python's `format()` does.
pub fn format_float(d : Double, spec_text : StringView) -> String raise Refused {
  let spec = parse_spec(spec_text)
  format_float_with(d, spec, false)
}

///|
/// `format(value, spec)`, for any number.
pub fn format(value : Number, spec_text : StringView) -> String raise Refused {
  match value {
    Int(n) => format_int(n, spec_text)
    Float(d) => format_float(d, spec_text)
    Fraction(f) => f.format(spec_text)
    Complex(z) => z.format(spec_text)
  }
}

///|
/// Format a complex number: the real part with the specifier's sign, the
/// imaginary part forced positive, then `j`, in parentheses unless the real
/// part is a positive zero.
pub fn Complex::format(
  self : Complex,
  spec_text : StringView,
) -> String raise Refused {
  let spec = parse_spec(spec_text)
  if spec.zero {
    malformed(spec.ty_at, "zero padding is not allowed for complex numbers")
  }
  if spec.align == '=' {
    malformed(spec.ty_at, "'=' alignment is not allowed for complex numbers")
  }
  let mut ty = spec.ty
  let mut skip_real = false
  let mut parens = false
  if ty == '\u{0}' {
    ty = 'r'
    skip_real = self.re == 0.0 && sign_of(self.re) > 0.0
    parens = !skip_real
  }
  let mut precision = spec.precision
  if ty == 'r' && precision >= 0 {
    ty = 'g'
  }
  if ty == 'n' {
    ty = 'g'
  }
  if ty != 'r' {
    match ty {
      'e' | 'E' | 'f' | 'F' | 'g' | 'G' => ()
      _ => malformed(spec.ty_at, "unknown format code for a complex number")
    }
  }
  if precision < 0 {
    precision = if ty == 'r' { 0 } else { 6 }
  }
  let mut re_text = if skip_real && !parens {
    ""
  } else {
    float_component(
      self.re,
      ty,
      precision,
      spec.alt,
      spec.neg0,
      spec.sign == '+',
    )
  }
  if re_text.length() > 0 &&
    spec.sign == ' ' &&
    re_text.get_char(0) != Some('-') &&
    re_text.get_char(0) != Some('+') {
    re_text = " " + re_text
  }
  let mut im_text = float_component(
    self.im,
    ty,
    precision,
    spec.alt,
    spec.neg0,
    if skip_real {
      spec.sign == '+'
    } else {
      true
    },
  )
  if skip_real &&
    spec.sign == ' ' &&
    im_text.get_char(0) != Some('-') &&
    im_text.get_char(0) != Some('+') {
    im_text = " " + im_text
  }
  let mut text = if skip_real { "" } else { re_text }
  if parens {
    text = "(" + text + im_text + "j)"
  } else {
    text = text + im_text + "j"
  }
  if spec.width > text.length() {
    layout_text(text, spec)
  } else {
    text
  }
}

///|
/// Format a fraction. Without a presentation type it is a ratio, with a digit
/// grouping separator allowed in both halves; with a float type it is the exact
/// rational rounded to the requested digits.
pub fn Fraction::format(
  self : Fraction,
  spec_text : StringView,
) -> String raise Refused {
  let spec = parse_spec(spec_text)
  match spec.ty {
    '\u{0}' => fraction_as_ratio(self, spec)
    'e' | 'E' | 'f' | 'F' | 'g' | 'G' | '%' => fraction_as_float(self, spec)
    'n' => fraction_as_float(self, spec)
    _ => malformed(spec.ty_at, "unknown format code for a fraction")
  }
}

///|
/// The no-type form: `3/4`, with grouping in both halves when asked.
fn fraction_as_ratio(f : Fraction, spec : Spec) -> String raise Refused {
  if spec.zero {
    malformed(spec.ty_at, "zero padding is not supported for fractions")
  }
  if spec.neg0 {
    malformed(
      spec.ty_at,
      "negative-zero coercion is not supported for fractions",
    )
  }
  if spec.precision >= 0 {
    malformed(
      spec.ty_at,
      "precision is not supported without a presentation type",
    )
  }
  let negative = f.num < 0N
  let magnitude = if negative { -f.num } else { f.num }
  let sep = match spec.group {
    ',' => ","
    '_' => "_"
    _ => ""
  }
  let top = if sep == "" {
    magnitude.to_string()
  } else {
    group_digits(magnitude.to_string(), sep.get_char(0).unwrap(), 3)
  }
  let body = if f.den > 1N || spec.alt {
    let bottom = if sep == "" {
      f.den.to_string()
    } else {
      group_digits(f.den.to_string(), sep.get_char(0).unwrap(), 3)
    }
    top + "/" + bottom
  } else {
    top
  }
  let sign = if negative {
    '-'
  } else {
    match spec.sign {
      '+' => '+'
      ' ' => ' '
      _ => '\u{0}'
    }
  }
  let sign_text = if sign == '\u{0}' { "" } else { sign.to_string() }
  let padding = spec.width - sign_text.length() - body.length()
  if padding <= 0 {
    return sign_text + body
  }
  match spec.align {
    '<' => sign_text + body + repeat_char(spec.fill, padding)
    '^' => {
      let left = padding / 2
      repeat_char(spec.fill, left) +
      sign_text +
      body +
      repeat_char(spec.fill, padding - left)
    }
    '=' => sign_text + repeat_char(spec.fill, padding) + body
    _ => repeat_char(spec.fill, padding) + sign_text + body
  }
}

///|
/// The float-type form: the exact rational, rounded half to even.
fn fraction_as_float(f : Fraction, spec : Spec) -> String {
  let ty = if spec.ty == 'n' { 'g' } else { spec.ty }
  let precision = if spec.precision < 0 { 6 } else { spec.precision }
  let negative = f.num < 0N
  let magnitude = if negative { -f.num } else { f.num }
  let mut digits = "0"
  let mut point_pos = precision
  let mut exponent = 0
  let mut scientific = false
  if ty == 'f' || ty == 'F' || ty == '%' {
    let place = if ty == '%' { precision + 2 } else { precision }
    let q = if magnitude == 0N {
      0N
    } else {
      div_round(magnitude * pow10(place), f.den)
    }
    digits = q.to_string()
    point_pos = precision
  } else {
    let figures = if ty == 'g' || ty == 'G' {
      if precision < 1 {
        1
      } else {
        precision
      }
    } else {
      precision + 1
    }
    let (sig, decpt) = if magnitude == 0N {
      ("0", 1)
    } else {
      sig_digits(magnitude, f.den, figures)
    }
    digits = sig
    exponent = decpt - figures
    scientific = ty == 'e' || ty == 'E' || exponent > 0 || decpt <= -4
    point_pos = if scientific { figures - 1 } else { figures - decpt }
  }
  if point_pos + 1 > digits.length() {
    digits = repeat_char('0', point_pos + 1 - digits.length()) + digits
  }
  let split_at = digits.length() - point_pos
  let leading = slice(digits, 0, split_at)
  let mut frac = slice(digits, split_at, digits.length())
  let trim_zeros = (ty == 'g' || ty == 'G') && !spec.alt
  if trim_zeros {
    frac = strip_zeros_or_empty(frac)
  }
  let mut suffix = ""
  if ty == '%' {
    suffix = "%"
  } else if scientific {
    let letter = if ty == 'E' || ty == 'F' || ty == 'G' { "E" } else { "e" }
    suffix = letter + signed_exponent(exponent + point_pos)
  }
  let separator = if !spec.alt && frac == "" { "" } else { "." }
  let trailing = separator + frac + suffix
  let sign = if negative {
    '-'
  } else {
    match spec.sign {
      '+' => '+'
      ' ' => ' '
      _ => '\u{0}'
    }
  }
  let sign_text = if sign == '\u{0}' { "" } else { sign.to_string() }
  let (sep, size) = grouping_for(spec, 'g')
  let mut body = leading
  if spec.zero && spec.width > 0 {
    let target = spec.width - sign_text.length() - trailing.length()
    if grouped_len(body.length(), if sep == '\u{0}' { 1 } else { size }) <
      target &&
      target > body.length() {
      let mut d = body.length()
      while grouped_len(d, size) < target {
        d += 1
      }
      body = repeat_char('0', d - body.length()) + body
    }
  }
  if sep != '\u{0}' {
    body = group_digits(body, sep, size)
  }
  let text = body + trailing
  let padded = spec.width - sign_text.length() - text.length()
  if padded <= 0 {
    return sign_text + text
  }
  match spec.align {
    '<' => sign_text + text + repeat_char(spec.fill, padded)
    '^' => {
      let left = padded / 2
      repeat_char(spec.fill, left) +
      sign_text +
      text +
      repeat_char(spec.fill, padded - left)
    }
    '=' => sign_text + repeat_char(spec.fill, padded) + text
    _ => repeat_char(spec.fill, padded) + sign_text + text
  }
}

///|
/// A decimal exponent written with a sign and at least two digits: `+02`,
/// `-01`, `+123`.
fn signed_exponent(exp : Int32) -> String {
  let sign = if exp < 0 { "-" } else { "+" }
  let magnitude = if exp < 0 { -exp } else { exp }
  if magnitude < 10 {
    sign + "0" + magnitude.to_string()
  } else {
    sign + magnitude.to_string()
  }
}

///|
/// Strip trailing zeros, allowing an empty result.
fn strip_zeros_or_empty(text : String) -> String {
  let mut n = text.length()
  while n > 0 && text.get_char(n - 1) == Some('0') {
    n -= 1
  }
  slice(text, 0, n)
}