///|
fn format_part(part : FormatPart, values : Array[Json]) -> String {
  let sb = StringBuilder::new()
  match part {
    LocField(loc~, spec~) => {
      let value = values[loc]
      let string = match spec.typ {
        Digit | Binary | Octal | HexLower | HexUpper =>
          format_digit(spec, value)
        Float | ExponentLower | ExponentUpper | Percent =>
          format_float(spec, value)
        Default =>
          match value {
            Json::String(s) => s
            Json::Number(n) => n.to_string()
            Json::True => "true"
            Json::False => "false"
            Json::Null => "None"
            Json::Array(arr) => arr.to_string()
            Json::Object(obj) => obj.to_string()
          }
      }
      align_string(spec, string, sb)
    }
    Text(text~) => sb.write_string(text)
  }
  sb.to_string()
}

///|
fn align_string(spec : FormatSpec, value : String, sb : StringBuilder) -> Unit {
  let width = spec.width.or(0)
  let mut value = value
  let padding = width - value.length()
  let fill = spec.options.fill.or(' ')
  if spec.options.sharp {
    value = match spec.typ {
      SpecType::Binary => "0b" + value
      SpecType::Octal => "0o" + value
      SpecType::HexLower => "0x" + value
      SpecType::HexUpper => "0X" + value
      _ => value
    }
  }
  if spec.options.zero {
    sb.write_string(value.pad_start(width, '0'))
    return
  }
  if padding > 0 {
    match spec.options.align {
      Align::Left => sb.write_string(value.pad_end(padding, fill))
      Align::Right => sb.write_string(value.pad_start(padding, fill))
      Align::Center => {
        let left_padding = padding / 2 + value.length() / 2
        sb.write_string(
          value.pad_start(left_padding, fill).pad_end(padding, fill),
        )
      }
    }
  } else {
    sb.write_string(value)
  }
}

///|
fn format_float(spec : FormatSpec, value : Json) -> String {
  if not(value is Json::Number(_)) {
    abort("format_float expects a number")
  }
  let value = value.as_number().unwrap()
  let doble_string = match spec.typ {
    Float =>
      if not(spec.precision is None) {
        @ryu.ryu_to_string_precision(value, precision=spec.precision.unwrap())
      } else {
        value.to_string() // Default is JavaScript specific
      }
    ExponentLower =>
      @ryu.ryu_to_string_exp(value, precision=spec.precision.or(6), upper=false)
    ExponentUpper =>
      @ryu.ryu_to_string_exp(value, precision=spec.precision.or(6), upper=true)
    Percent => {
      let percent_value = value * 100.0
      @ryu.ryu_to_string_precision(
        percent_value,
        precision=spec.precision.or(2),
      ) +
      "%"
    }
    _ => value.to_string()
  }
  doble_string
}

///|
fn format_digit(spec : FormatSpec, value : Json) -> String {
  if not(value is Json::Number(_)) {
    abort("format_digit expects a number")
  }
  let value = value.as_number().unwrap().to_int64()
  let mut num_string = match spec.typ {
    Binary => value.to_string(radix=2)
    Octal => value.to_string(radix=8)
    HexLower => value.to_string(radix=16)
    HexUpper => value.to_string(radix=16).to_upper()
    Digit => value.to_string()
    _ => value.to_string()
  }
  let digit_group = fn(grouping : Char, num_string : String, cnt : Int) {
    let new_num_string = StringBuilder::new()
    for idx, char in num_string {
      if idx > 0 && idx % cnt == 0 {
        new_num_string.write_char(grouping)
      }
      new_num_string.write_char(char)
    }
    new_num_string.to_string()
  }
  if spec.grouping is Default {
    return num_string
  }
  num_string = match (spec.grouping, spec.typ) {
    (Comma, Digit) => digit_group(',', num_string, 3)
    (Underscore, Digit) => digit_group('_', num_string, 3)
    (Underscore, Octal | HexLower | HexUpper | Binary) =>
      // For underscore grouping, we can apply it to binary, octal, and hex
      digit_group('_', num_string, 4)
    (_, _) => abort("Unsupported grouping for this type")
  }
  num_string
}

///|
fn format(parts : Array[FormatPart], values : Array[&Formatter]) -> String {
  let result = StringBuilder::new()
  for part in parts {
    match part {
      LocField(loc~, spec~) => {
        let value = values[loc]
        value.display(spec, result)
      }
      Text(text~) => result.write_string(text)
    }
  }
  result.to_string()
}

///|
/// Formats a string with placeholders using the provided formatter values.
///
/// Parses the format string to identify placeholders and literal text, then
/// applies the formatter values to fill the placeholders according to their
/// specifications. This function provides a flexible way to create formatted
/// strings with type-safe value insertion.
///
/// Parameters:
///
/// * `parts` : The format string containing placeholders (e.g., `{0}`, `{1:x}`)
/// and literal text to be formatted.
/// * `values` : An array of formatter objects that implement the `Formatter`
/// trait, used to fill the placeholders in the format string.
///
/// Returns the formatted string with all placeholders replaced by their
/// corresponding formatted values.
///
/// Example:
///
/// ```moonbit
/// let name = "Alice"
/// let age = 30
/// let formatted = @fmt.fstring("Hello, {0}! You are {1} years old.", [name, age])
/// inspect(formatted, content="Hello, Alice! You are 30 years old.")
/// ```
///
pub fn fstring(parts : String, values : Array[&Formatter]) -> String {
  let parts = parse_format_string(parts)
  format(parts, values)
}

///|
/// Formats a string with placeholders using the provided formatter values and
/// prints it to standard output followed by a newline.
///
/// This function combines string formatting and output in a single operation. It
/// parses the format string, applies the formatter values to fill placeholders,
/// and outputs the result.
///
/// Parameters:
///
/// * `parts` : The format string containing placeholders and literal text to be
/// formatted.
/// * `values` : An array of formatter objects that implement the `Formatter`
/// trait, used to fill the placeholders in the format string.
///
/// Example:
///
/// ```moonbit
/// let name = "Alice"
/// let age = 30
/// @fmt.fprintln("Hello, {0}! You are {1} years old.", [name, age])
/// // Outputs: Hello, Alice! You are 30 years old.
/// ```
///
pub fn fprintln(parts : String, values : Array[&Formatter]) -> Unit {
  fstring(parts, values) |> println
}

///|
fn jformat(parts : Array[FormatPart], values : Array[Json]) -> String {
  let result = StringBuilder::new()
  for part in parts {
    let formatted_value = format_part(part, values)
    result.write_string(formatted_value)
  }
  result.to_string()
}

///|
/// Formats a string with placeholders using the provided JSON values.
///
/// Parses the format string to identify placeholders and literal text, then
/// applies the JSON values to fill the placeholders according to their
/// specifications. This function provides a flexible way to create formatted
/// strings using JSON data as input.
///
/// Parameters:
///
/// * `parts` : The format string containing placeholders (e.g., `{0}`, `{1:x}`)
///  and literal text to be formatted.
/// * `values` : A JSON array containing the values used to fill the placeholders
///  in the format string. Each element corresponds to a placeholder position.
///
/// Returns the formatted string with all placeholders replaced by their
/// corresponding formatted values from the JSON array.
///
/// Panics if `values` is not a JSON array.
///
/// Example:
///
/// ```moonbit
/// let data = @json.parse("[\"Alice\", 30]")
/// let formatted = @fmt.jstring("Hello, {0}! You are {1} years old.", data)
/// inspect(formatted, content="Hello, Alice! You are 30 years old.")
/// ```
///
pub fn jstring(parts : String, values : Json) -> String {
  let parts = parse_format_string(parts)
  match values {
    Json::Array(_) => ()
    _ => abort("fprintln expects an array of values")
  }
  jformat(parts, values.as_array().unwrap())
}

///|
/// Formats a string with placeholders using the provided JSON values and prints
/// it to standard output followed by a newline.
///
/// This function combines string formatting and output in a single operation. It
/// parses the format string, applies the JSON values to fill placeholders
/// according to their specifications, and outputs the result to the console.
///
/// Parameters:
///
/// * `parts` : The format string containing placeholders (e.g., `{0}`, `{1:x}`)
/// and literal text to be formatted.
/// * `values` : A JSON array containing the values used to fill the placeholders
/// in the format string. Each element corresponds to a placeholder position.
///
/// Panics if `values` is not a JSON array.
///
/// Example:
///
/// ```moonbit
/// let data = @json.parse("[\"Alice\", 30]")
/// @fmt.jprintln("Hello, {0}! You are {1} years old.", data)
/// // Outputs: Hello, Alice! You are 30 years old.
/// ```
///
pub fn jprintln(parts : String, values : Json) -> Unit {
  jstring(parts, values) |> println
}