///|
/// A semantic reason a parsed value would change under JCS normalization.
pub enum CanonicalIssueKind {
  NumberSpelling
  ObjectMemberOrder
}

///|
pub(all) struct CanonicalIssue {
  path : String
  kind : CanonicalIssueKind
  source : String
  canonical : String
}

///|
pub fn CanonicalIssueKind::to_string(self : CanonicalIssueKind) -> String {
  match self {
    NumberSpelling => "number-spelling"
    ObjectMemberOrder => "object-member-order"
  }
}

///|
/// Inspect parsed values for semantic normalization changes. Whitespace and
/// string escaping require source text and are covered by `check_canonical`;
/// this API focuses on values where the parser has already discarded layout.
pub fn canonical_issues(value : JsonValue) -> Array[CanonicalIssue] {
  let issues : Array[CanonicalIssue] = []
  scan_issues(value, "", issues)
  issues
}

///|
pub fn diagnose_document(
  input : String,
) -> Result[Array[CanonicalIssue], String] {
  let value = match parse(input) {
    Ok(value) => value
    Err(error) => return Err(error.message())
  }
  Ok(canonical_issues(value))
}

///|
pub fn CanonicalIssue::to_text(self : CanonicalIssue) -> String {
  self.kind.to_string() +
  " " +
  (if self.path == "" { "$" } else { self.path }) +
  ": " +
  self.source +
  " -> " +
  self.canonical
}

///|
fn scan_issues(
  value : JsonValue,
  path : String,
  issues : Array[CanonicalIssue],
) -> Unit {
  match value {
    Number(source) =>
      match canonical_number(source) {
        Ok(canonical) =>
          if source != canonical {
            issues.push({ path, kind: NumberSpelling, source, canonical })
          }
        Err(_) => ()
      }
    Array(values) => {
      let mut index = 0
      while index < values.length() {
        scan_issues(values[index], path + "/" + index.to_string(), issues)
        index = index + 1
      }
    }
    Object(entries) => {
      let mut previous : String? = None
      for entry in entries {
        match previous {
          Some(key) =>
            if compare_keys(key, entry.0) > 0 {
              issues.push({
                path,
                kind: ObjectMemberOrder,
                source: key + " before " + entry.0,
                canonical: "UTF-16 ascending order",
              })
            }
          None => ()
        }
        previous = Some(entry.0)
        scan_issues(
          entry.1,
          path + "/" + diagnostic_pointer_escape(entry.0),
          issues,
        )
      }
    }
    _ => ()
  }
}

///|
fn diagnostic_pointer_escape(value : String) -> String {
  let mut output = ""
  for char in value {
    if char == '~' {
      output = output + "~0"
    } else if char == '/' {
      output = output + "~1"
    } else {
      output = output + char.to_string()
    }
  }
  output
}