///|
/// The semantic kind of a difference between two JSON values.
pub enum DiffKind {
  Added
  Removed
  Changed
  TypeChanged
}

///|
/// One difference, addressed by an RFC 6901-style JSON Pointer.
pub(all) struct JsonDiff {
  path : String
  kind : DiffKind
  left : String?
  right : String?
}

///|
pub fn DiffKind::to_string(self : DiffKind) -> String {
  match self {
    Added => "added"
    Removed => "removed"
    Changed => "changed"
    TypeChanged => "type-changed"
  }
}

///|
/// Compare two parsed documents semantically. Object member order is ignored,
/// while arrays remain ordered. The result is stable in left-to-right path
/// order, which makes it suitable for review output and snapshot tests.
pub fn diff_json(left : JsonValue, right : JsonValue) -> Array[JsonDiff] {
  let differences : Array[JsonDiff] = []
  diff_value(left, right, "", differences)
  differences
}

///|
/// Parse and compare two documents, returning parser diagnostics as text.
pub fn diff_documents(
  left : String,
  right : String,
) -> Result[Array[JsonDiff], String] {
  let left_value = match parse(left) {
    Ok(value) => value
    Err(error) => return Err("left document: " + error.message())
  }
  let right_value = match parse(right) {
    Ok(value) => value
    Err(error) => return Err("right document: " + error.message())
  }
  Ok(diff_json(left_value, right_value))
}

///|
fn diff_value(
  left : JsonValue,
  right : JsonValue,
  path : String,
  differences : Array[JsonDiff],
) -> Unit {
  if canonical_equal(left, right) {
    return
  }
  match (left, right) {
    (Array(left_values), Array(right_values)) => {
      let shared = if left_values.length() < right_values.length() {
        left_values.length()
      } else {
        right_values.length()
      }
      let mut index = 0
      while index < shared {
        diff_value(
          left_values[index],
          right_values[index],
          pointer_index(path, index),
          differences,
        )
        index = index + 1
      }
      while index < left_values.length() {
        differences.push({
          path: pointer_index(path, index),
          kind: Removed,
          left: Some(value_preview(left_values[index])),
          right: None,
        })
        index = index + 1
      }
      while index < right_values.length() {
        differences.push({
          path: pointer_index(path, index),
          kind: Added,
          left: None,
          right: Some(value_preview(right_values[index])),
        })
        index = index + 1
      }
    }
    (Object(left_entries), Object(right_entries)) => {
      for entry in left_entries {
        let key = entry.0
        match object_value(right_entries, key) {
          Some(right_value) =>
            diff_value(
              entry.1,
              right_value,
              pointer_key(path, key),
              differences,
            )
          None =>
            differences.push({
              path: pointer_key(path, key),
              kind: Removed,
              left: Some(value_preview(entry.1)),
              right: None,
            })
        }
      }
      for entry in right_entries {
        if object_value(left_entries, entry.0) is None {
          differences.push({
            path: pointer_key(path, entry.0),
            kind: Added,
            left: None,
            right: Some(value_preview(entry.1)),
          })
        }
      }
    }
    _ => {
      let kind = if value_kind(left) == value_kind(right) {
        Changed
      } else {
        TypeChanged
      }
      differences.push({
        path,
        kind,
        left: Some(value_preview(left)),
        right: Some(value_preview(right)),
      })
    }
  }
}

///|
fn canonical_equal(left : JsonValue, right : JsonValue) -> Bool {
  match (canonicalize(left), canonicalize(right)) {
    (Ok(left_text), Ok(right_text)) => left_text == right_text
    _ => false
  }
}

///|
fn object_value(
  entries : Array[(String, JsonValue)],
  key : String,
) -> JsonValue? {
  for entry in entries {
    if entry.0 == key {
      return Some(entry.1)
    }
  }
  None
}

///|
fn pointer_key(path : String, key : String) -> String {
  path + "/" + pointer_escape(key)
}

///|
fn pointer_index(path : String, index : Int) -> String {
  path + "/" + index.to_string()
}

///|
fn 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
}

///|
fn value_kind(value : JsonValue) -> String {
  match value {
    Null => "null"
    Bool(_) => "boolean"
    Number(_) => "number"
    String(_) => "string"
    Array(_) => "array"
    Object(_) => "object"
  }
}

///|
fn value_preview(value : JsonValue) -> String {
  match canonicalize(value) {
    Ok(serialized) => serialized
    Err(_) => ""
  }
}

///|
pub fn JsonDiff::to_text(self : JsonDiff) -> String {
  let left = match self.left {
    Some(value) => value
    None => "-"
  }
  let right = match self.right {
    Some(value) => value
    None => "-"
  }
  self.kind.to_string() + " " + self.path + " : " + left + " -> " + right
}