///|
/// Character variants only: no colloquial magnitude inference or text extraction.
fn numeral_variants(source : String) -> String {
  let out = StringBuilder()
  for c in source.iter() {
    out.write_char(
      match c {
        '負' => '负'
        '萬' => '万'
        '億' => '亿'
        '貳' => '贰'
        '參' => '叁'
        '陸' => '陆'
        '兩' | '两' => '二'
        '〇' => '零'
        '點' => '点'
        _ => c
      },
    )
  }
  out.to_string()
}

///|
/// Validate a signed decimal string without floating point or exponent notation.
fn decimal_parts(source : String) -> (Bool, String, String?) raise NumberError {
  if source.is_empty() || source.length() > 256 {
    raise Invalid("decimal length 1..256")
  }
  let negative = source.has_prefix("-")
  let body = if negative { source[1:].to_owned() } else { source }
  let parts = body.split(".").collect()
  if parts.length() > 2 || parts.iter().any(p => p.is_empty()) {
    raise Invalid("decimal requires digits on both sides of the point")
  }
  for part in parts {
    if !part.iter().all(c => c >= '0' && c <= '9') {
      raise Invalid("ASCII decimal digits required")
    }
  }
  (
    negative,
    parts[0].to_owned(),
    if parts.length() == 2 {
      Some(parts[1].to_owned())
    } else {
      None
    },
  )
}

///|
/// Read a digit sequence, retaining leading zeros, fraction scale and negative zero.
pub fn format_digits(
  source : String,
  financial? : Bool = false,
) -> String raise NumberError {
  ignore(decimal_parts(source))
  let names = digits(financial)
  let out = StringBuilder()
  for c in source.iter() {
    out.write_string(
      match c {
        '-' => "负"
        '.' => "点"
        _ => names[c.to_int() - 48]
      },
    )
  }
  out.to_string()
}

///|
/// Direct digit reading: 一二点三零 -> 12.30; unit words such as 十 are rejected.
pub fn parse_digits(
  source : String,
  variants? : Bool = false,
) -> String raise NumberError {
  if source.is_empty() || source.length() > 256 {
    raise Invalid("digit text length 1..256")
  }
  let source = normalize(
    if variants {
      numeral_variants(source)
    } else {
      source
    },
  )
  let names = digits(false)
  let out = StringBuilder()
  for c in source.iter() {
    if c == '负' {
      out.write_char('-')
      continue
    }
    if c == '点' {
      out.write_char('.')
      continue
    }
    let mut digit = -1
    for i, name in names {
      if name == c.to_string() {
        digit = i
        break
      }
    }
    if digit < 0 {
      raise Invalid("expected individual Chinese digit")
    }
    out.write_char((digit + 48).unsafe_to_char())
  }
  let result = out.to_string()
  ignore(decimal_parts(result))
  result
}

///|
/// Unit-based integer part plus up to 64 exact fractional digits. Retains trailing zeros.
/// Leading integer zeros are normalized; explicit negative zero is retained.
pub fn format_decimal(
  source : String,
  financial? : Bool = false,
) -> String raise NumberError {
  let (negative, whole, fraction) = decimal_parts(source)
  let mut value = 0L
  for c in whole.iter() {
    let digit = (c.to_int() - 48).to_int64()
    if value > (9999999999999999L - digit) / 10L {
      raise Invalid("integer outside supported range")
    }
    value = value * 10L + digit
  }
  let mut result = (if negative { "负" } else { "" }) +
    format_integer(value, financial~)
  if fraction is Some(text) {
    if text.length() > 64 {
      raise Invalid("at most 64 fractional digits")
    }
    result += "点" + format_digits(text, financial~)
  }
  result
}

///|
/// Returns an exact ASCII decimal string; never rounds through a floating-point value.
pub fn parse_decimal(
  source : String,
  variants? : Bool = false,
) -> String raise NumberError {
  if source.is_empty() || source.length() > 256 {
    raise Invalid("decimal text length 1..256")
  }
  let source = if variants { numeral_variants(source) } else { source }
  let negative = source.has_prefix("负")
  let body = if negative { source[1:].to_owned() } else { source }
  let parts = body.split("点").collect()
  if parts.length() > 2 ||
    parts.iter().any(p => p.is_empty()) ||
    body.contains("负") {
    raise Invalid("invalid Chinese decimal")
  }
  let whole = parse_integer(parts[0].to_owned())
  let mut result = (if negative { "-" } else { "" }) + whole.to_string()
  if parts.length() == 2 {
    let fraction = parse_digits(parts[1].to_owned())
    if fraction.length() > 64 ||
      fraction.contains("-") ||
      fraction.contains(".") {
      raise Invalid("invalid decimal fraction")
    }
    result += "." + fraction
  }
  result
}

///|
/// Complement of parse_decimal_cents: always exactly two decimal digits.
pub fn format_decimal_cents(cents : Int64) -> String raise NumberError {
  if cents < -9999999999999999L || cents > 9999999999999999L {
    raise Invalid("amount outside supported range")
  }
  let amount = if cents < 0L { -cents } else { cents }
  let fraction = (amount % 100L).to_int()
  (if cents < 0L { "-" } else { "" }) +
  (amount / 100L).to_string() +
  "." +
  (if fraction < 10 { "0" } else { "" }) +
  fraction.to_string()
}