///|
/// A JSON Patch-like operation over a parsed value. Paths use RFC 6901 JSON
/// Pointer syntax, and operations are applied in array order.
pub enum PatchOperation {
  Add(String, JsonValue)
  Remove(String)
  Replace(String, JsonValue)
}

///|
pub fn patch_add(path : String, value : JsonValue) -> PatchOperation {
  PatchOperation::Add(path, value)
}

///|
pub fn patch_remove(path : String) -> PatchOperation {
  PatchOperation::Remove(path)
}

///|
pub fn patch_replace(path : String, value : JsonValue) -> PatchOperation {
  PatchOperation::Replace(path, value)
}

///|
pub fn PatchOperation::to_text(self : PatchOperation) -> String {
  match self {
    Add(path, value) => "add " + path + " " + value_preview(value)
    Remove(path) => "remove " + path
    Replace(path, value) => "replace " + path + " " + value_preview(value)
  }
}

///|
pub fn PatchOperation::to_json(self : PatchOperation) -> String {
  match self {
    Add(path, value) =>
      "{\"op\":\"add\",\"path\":" +
      canonical_string(path) +
      ",\"value\":" +
      value_preview(value) +
      "}"
    Remove(path) =>
      "{\"op\":\"remove\",\"path\":" + canonical_string(path) + "}"
    Replace(path, value) =>
      "{\"op\":\"replace\",\"path\":" +
      canonical_string(path) +
      ",\"value\":" +
      value_preview(value) +
      "}"
  }
}

///|
pub fn apply_patch(
  value : JsonValue,
  operations : Array[PatchOperation],
) -> Result[JsonValue, String] {
  let mut current = value
  for operation in operations {
    current = match apply_operation(current, operation) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
  }
  Ok(current)
}

///|
pub fn apply_patch_documents(
  input : String,
  operations : Array[PatchOperation],
) -> Result[String, String] {
  let value = match parse(input) {
    Ok(value) => value
    Err(error) => return Err(error.message())
  }
  match apply_patch(value, operations) {
    Ok(value) =>
      match canonicalize(value) {
        Ok(output) => Ok(output)
        Err(error) => Err(error.message())
      }
    Err(error) => Err(error)
  }
}

///|
fn apply_operation(
  value : JsonValue,
  operation : PatchOperation,
) -> Result[JsonValue, String] {
  let (path, action) = match operation {
    Add(path, value) => (path, PatchAction::Add(value))
    Remove(path) => (path, PatchAction::Remove)
    Replace(path, value) => (path, PatchAction::Replace(value))
  }
  let segments = match parse_pointer(path) {
    Ok(segments) => segments
    Err(error) => return Err(error)
  }
  patch_at(value, segments, 0, action)
}

///|
priv enum PatchAction {
  Add(JsonValue)
  Remove
  Replace(JsonValue)
}

///|
fn patch_at(
  value : JsonValue,
  segments : Array[String],
  depth : Int,
  action : PatchAction,
) -> Result[JsonValue, String] {
  if depth == segments.length() {
    match action {
      Add(value) | Replace(value) => Ok(value)
      Remove => Err("cannot remove the document root")
    }
  } else {
    let segment = segments[depth]
    match value {
      Object(entries) => patch_object(entries, segments, depth, segment, action)
      Array(values) => patch_array(values, segments, depth, segment, action)
      _ =>
        Err("cannot apply a patch below scalar path segment '" + segment + "'")
    }
  }
}

///|
fn patch_object(
  entries : Array[(String, JsonValue)],
  segments : Array[String],
  depth : Int,
  segment : String,
  action : PatchAction,
) -> Result[JsonValue, String] {
  let final_segment = depth + 1 == segments.length()
  let updated : Array[(String, JsonValue)] = []
  let mut found = false
  for entry in entries {
    if entry.0 != segment {
      updated.push(entry)
    } else {
      found = true
      if final_segment {
        match action {
          Add(value) | Replace(value) => updated.push((segment, value))
          Remove => ()
        }
      } else {
        let child = match patch_at(entry.1, segments, depth + 1, action) {
          Ok(value) => value
          Err(error) => return Err(error)
        }
        updated.push((segment, child))
      }
    }
  }
  if !found {
    if final_segment {
      match action {
        Add(value) => updated.push((segment, value))
        Replace(_) =>
          return Err("cannot replace missing object key: " + segment)
        Remove => return Err("cannot remove missing object key: " + segment)
      }
    } else {
      return Err("object key not found: " + segment)
    }
  }
  Ok(Object(updated))
}

///|
fn patch_array(
  values : Array[JsonValue],
  segments : Array[String],
  depth : Int,
  segment : String,
  action : PatchAction,
) -> Result[JsonValue, String] {
  let index = if segment == "-" && depth + 1 == segments.length() {
    values.length()
  } else {
    match parse_array_index(segment) {
      Some(index) => index
      None => return Err("array patch segment is not a valid index: " + segment)
    }
  }
  let final_segment = depth + 1 == segments.length()
  if final_segment {
    match action {
      Add(value) => {
        if index > values.length() {
          return Err("array add index out of range: " + segment)
        }
        Ok(Array(array_insert(values, index, value)))
      }
      Replace(value) => {
        if index >= values.length() {
          return Err("array replace index out of range: " + segment)
        }
        Ok(Array(array_replace(values, index, value)))
      }
      Remove => {
        if index >= values.length() {
          return Err("array remove index out of range: " + segment)
        }
        Ok(Array(array_remove(values, index)))
      }
    }
  } else {
    if index >= values.length() {
      return Err("array index out of range: " + segment)
    }
    let child = match patch_at(values[index], segments, depth + 1, action) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    Ok(Array(array_replace(values, index, child)))
  }
}

///|
fn array_insert(
  values : Array[JsonValue],
  index : Int,
  value : JsonValue,
) -> Array[JsonValue] {
  let output : Array[JsonValue] = []
  let mut cursor = 0
  while cursor < values.length() {
    if cursor == index {
      output.push(value)
    }
    output.push(values[cursor])
    cursor = cursor + 1
  }
  if index == values.length() {
    output.push(value)
  }
  output
}

///|
fn array_replace(
  values : Array[JsonValue],
  index : Int,
  value : JsonValue,
) -> Array[JsonValue] {
  let output : Array[JsonValue] = []
  let mut cursor = 0
  for item in values {
    output.push(if cursor == index { value } else { item })
    cursor = cursor + 1
  }
  output
}

///|
fn array_remove(values : Array[JsonValue], index : Int) -> Array[JsonValue] {
  let output : Array[JsonValue] = []
  let mut cursor = 0
  for item in values {
    if cursor != index {
      output.push(item)
    }
    cursor = cursor + 1
  }
  output
}