///|
pub(all) enum PatchKind {
  Add
  Replace
  Remove
  AssertValue
  Copy
  MoveValue
} derive(Eq, Debug)

///|
pub(all) struct PatchOp {
  kind : PatchKind
  path : String
  value : Json?
  from : String?
} derive(Eq, Debug)

///|
pub(all) struct PatchError {
  index : Int
  path : String
  message : String
} derive(Eq, Debug)

///|
pub fn PatchOp::add(path : String, value : Json) -> PatchOp {
  { kind: Add, path, value: Some(value), from: None }
}

///|
pub fn PatchOp::replace(path : String, value : Json) -> PatchOp {
  { kind: Replace, path, value: Some(value), from: None }
}

///|
pub fn PatchOp::remove(path : String) -> PatchOp {
  { kind: Remove, path, value: None, from: None }
}

///|
pub fn PatchOp::assert_value(path : String, value : Json) -> PatchOp {
  { kind: AssertValue, path, value: Some(value), from: None }
}

///|
pub fn PatchOp::copy(from : String, path : String) -> PatchOp {
  { kind: Copy, path, value: None, from: Some(from) }
}

///|
pub fn PatchOp::move_to(from : String, path : String) -> PatchOp {
  { kind: MoveValue, path, value: None, from: Some(from) }
}

///|
pub fn PatchOp::parse_many(doc : Json) -> Result[Array[PatchOp], String] {
  match doc {
    Array(items) => {
      let ops : Array[PatchOp] = []
      for index, item in items {
        match PatchOp::parse(item) {
          Ok(op) => ops.push(op)
          Err(message) =>
            return Err("invalid patch operation \{index}: \{message}")
        }
      }
      Ok(ops)
    }
    _ => Err("patch document must be an array")
  }
}

///|
pub fn PatchOp::parse(doc : Json) -> Result[PatchOp, String] {
  let object = match doc {
    Object(object) => object
    _ => return Err("operation must be an object")
  }
  let op = match object.get("op") {
    Some(String(value)) => value
    _ => return Err("operation field 'op' must be a string")
  }
  let path = match object.get("path") {
    Some(String(value)) => value
    _ => return Err("operation field 'path' must be a string")
  }
  match op {
    "add" =>
      match object.get("value") {
        Some(value) => Ok(PatchOp::add(path, value))
        None => Err("add operation requires 'value'")
      }
    "replace" =>
      match object.get("value") {
        Some(value) => Ok(PatchOp::replace(path, value))
        None => Err("replace operation requires 'value'")
      }
    "remove" => Ok(PatchOp::remove(path))
    "test" =>
      match object.get("value") {
        Some(value) => Ok(PatchOp::assert_value(path, value))
        None => Err("test operation requires 'value'")
      }
    "copy" =>
      match object.get("from") {
        Some(String(from)) => Ok(PatchOp::copy(from, path))
        _ => Err("copy operation requires string 'from'")
      }
    "move" =>
      match object.get("from") {
        Some(String(from)) => Ok(PatchOp::move_to(from, path))
        _ => Err("move operation requires string 'from'")
      }
    _ => Err("unknown operation '\{op}'")
  }
}

///|
pub fn PatchError::describe(self : PatchError) -> String {
  "patch operation \{self.index} failed at \{self.path}: \{self.message}"
}

///|
pub fn apply_patch(
  doc : Json,
  ops : Array[PatchOp],
) -> Result[Json, PatchError] {
  let mut current = doc
  for index, op in ops {
    match apply_patch_op(current, op) {
      Ok(next) => current = next
      Err(message) => return Err({ index, path: patch_error_path(op), message })
    }
  }
  Ok(current)
}

///|
fn apply_patch_op(doc : Json, op : PatchOp) -> Result[Json, String] {
  match op.kind {
    Add =>
      match op.value {
        Some(value) => pointer_add(op.path, doc, value)
        None => Err("add operation requires a value")
      }
    Replace =>
      match op.value {
        Some(value) =>
          match Pointer::parse(op.path) {
            Ok(pointer) =>
              match pointer.set(doc, value) {
                Ok(next) => Ok(next)
                Err(err) => Err(err.message)
              }
            Err(err) => Err(err.message)
          }
        None => Err("replace operation requires a value")
      }
    Remove =>
      match Pointer::parse(op.path) {
        Ok(pointer) =>
          match pointer.remove(doc) {
            Ok(next) => Ok(next)
            Err(err) => Err(err.message)
          }
        Err(err) => Err(err.message)
      }
    AssertValue =>
      match (Pointer::parse(op.path), op.value) {
        (Ok(pointer), Some(expected)) =>
          match pointer.get(doc) {
            Ok(actual) =>
              if actual == expected {
                Ok(doc)
              } else {
                Err("test operation did not match")
              }
            Err(err) => Err(err.message)
          }
        (Err(err), _) => Err(err.message)
        (_, None) => Err("test operation requires a value")
      }
    Copy =>
      match op.from {
        Some(from) =>
          match Pointer::parse(from) {
            Ok(from_pointer) =>
              match from_pointer.get(doc) {
                Ok(value) => pointer_add(op.path, doc, value)
                Err(err) => Err(err.message)
              }
            Err(err) => Err(err.message)
          }
        None => Err("copy operation requires a source path")
      }
    MoveValue =>
      match op.from {
        Some(from) =>
          match Pointer::parse(from) {
            Ok(from_pointer) =>
              match from_pointer.get(doc) {
                Ok(value) =>
                  match from_pointer.remove(doc) {
                    Ok(without_source) =>
                      pointer_add(op.path, without_source, value)
                    Err(err) => Err(err.message)
                  }
                Err(err) => Err(err.message)
              }
            Err(err) => Err(err.message)
          }
        None => Err("move operation requires a source path")
      }
  }
}

///|
fn patch_error_path(op : PatchOp) -> String {
  match op.kind {
    Copy | MoveValue =>
      match op.from {
        Some(from) => "\{from} -> \{op.path}"
        None => op.path
      }
    _ => op.path
  }
}

///|
fn pointer_add(path : String, doc : Json, value : Json) -> Result[Json, String] {
  match Pointer::parse(path) {
    Ok(pointer) =>
      if pointer.parts.length() == 0 {
        Ok(value)
      } else {
        match add_at(doc, pointer.parts, 0, value) {
          Ok(next) => Ok(next)
          Err(err) => Err(err)
        }
      }
    Err(err) => Err(err.message)
  }
}

///|
fn add_at(
  current : Json,
  parts : Array[String],
  depth : Int,
  value : Json,
) -> Result[Json, String] {
  let part = parts[depth]
  let last = depth + 1 == parts.length()
  match current {
    Object(object) => {
      let copy = object.copy()
      if last {
        copy.set(part, value)
        Ok(Json::object(copy))
      } else {
        match object.get(part) {
          Some(child) =>
            match add_at(child, parts, depth + 1, value) {
              Ok(updated) => {
                copy.set(part, updated)
                Ok(Json::object(copy))
              }
              Err(message) => Err(message)
            }
          None => Err("object key was not found")
        }
      }
    }
    Array(array) =>
      if last {
        match insertion_index(part, array.length()) {
          Some(index) => Ok(Json::array(insert_json(array, index, value)))
          None => Err("array insertion token is not '-' or a valid index")
        }
      } else {
        match parse_patch_index(part, array.length()) {
          Some(index) =>
            match add_at(array[index], parts, depth + 1, value) {
              Ok(updated) => {
                let copy = array.copy()
                copy[index] = updated
                Ok(Json::array(copy))
              }
              Err(message) => Err(message)
            }
          None => Err("array index is out of bounds")
        }
      }
    _ => Err("cannot descend into a scalar JSON value")
  }
}

///|
fn insertion_index(token : String, len : Int) -> Int? {
  if token == "-" {
    return Some(len)
  }
  match parse_patch_index(token, len + 1) {
    Some(index) => if index <= len { Some(index) } else { None }
    None => None
  }
}

///|
fn parse_patch_index(token : String, len : Int) -> Int? {
  match parse_array_index(token) {
    Some(index) => if index >= 0 && index < len { Some(index) } else { None }
    None => None
  }
}

///|
fn insert_json(array : Array[Json], index : Int, value : Json) -> Array[Json] {
  let out : Array[Json] = []
  for i, item in array {
    if i == index {
      out.push(value)
    }
    out.push(item)
  }
  if index == array.length() {
    out.push(value)
  }
  out
}