///|
/// Compare decimal values without rounding large integers through Double.
/// Exponents are bounded to keep normalization arithmetic safe.
fn canonical_number(value : Json, repr : String?) -> String raise ConfigError {
  let text = match repr {
    Some(s) => s
    None => value.stringify()
  }
  let parsed = @json.parse(text) catch {
    _ => raise InvalidPatch("invalid JSON number")
  }
  if !(parsed is Number(_, ..)) {
    raise InvalidPatch("non-finite JSON number")
  }
  let lower = text.replace_all(old="E", new="e")
  let parts = lower.split("e").to_array()
  let exp = if parts.length() == 2 {
    @string.parse_int(parts[1]) catch {
      _ => raise LimitExceeded("number exponent out of range")
    }
  } else {
    0
  }
  if exp < -1000000 || exp > 1000000 {
    raise LimitExceeded("number exponent exceeds +/-1000000")
  }
  let mantissa = parts[0].to_owned()
  let negative = mantissa.strip_prefix("-") is Some(_)
  let unsigned = if negative {
    mantissa.strip_prefix("-").unwrap().to_owned()
  } else {
    mantissa
  }
  let decimal = unsigned.split(".").to_array()
  let digits = unsigned.replace_all(old=".", new="").to_array()
  let fraction = if decimal.length() == 2 { decimal[1].length() } else { 0 }
  let mut start = 0
  let mut end = digits.length()
  while start < end && digits[start] == '0' {
    start = start + 1
  }
  if start == end {
    return "0"
  }
  while end > start && digits[end - 1] == '0' {
    end = end - 1
  }
  let exponent = exp - fraction + digits.length() - end
  (if negative { "-" } else { "" }) +
  String::from_array(digits.exact_view(start~, end~)) +
  "e" +
  exponent.to_string()
}

///|
/// Semantic equality ignores object member order and numeric spelling.
/// Public traversal functions validate first; recursive calls cannot fail.
fn same(a : Json, b : Json) -> Bool {
  match (a, b) {
    (Number(_, repr=ar), Number(_, repr=br)) => {
      // Numbers have already been validated by each public operation.
      let x = canonical_number(a, ar) catch { _ => return false }
      let y = canonical_number(b, br) catch { _ => return false }
      x == y
    }
    (Array(x), Array(y)) => {
      if x.length() != y.length() {
        return false
      }
      for i = 0; i < x.length(); i = i + 1 {
        if !same(x[i], y[i]) {
          return false
        }
      }
      true
    }
    (Object(x), Object(y)) => {
      if x.length() != y.length() {
        return false
      }
      for k, v in x {
        guard y.get(k) is Some(w) else { return false }
        if !same(v, w) {
          return false
        }
      }
      true
    }
    _ => a == b
  }
}

///|
fn same_slot(a : Json?, b : Json?) -> Bool {
  match (a, b) {
    (Some(x), Some(y)) => same(x, y)
    (None, None) => true
    _ => false
  }
}