///|
pub fn json_key_count(value : Json) -> Int {
  match value {
    Object(fields) => fields.length()
    _ => 0
  }
}

///|
pub fn json_object_keys(value : Json) -> Array[String] {
  match value {
    Object(fields) => fields.keys().to_array()
    _ => []
  }
}

///|
pub fn json_array_length(value : Json) -> Int {
  match value {
    Array(items) => items.length()
    _ => 0
  }
}

///|
pub fn json_has_key(value : Json, key : String) -> Bool {
  match value {
    Object(fields) => fields.contains(key)
    _ => false
  }
}

///|
pub fn json_contains_text(value : Json, text : String) -> Bool {
  match value {
    String(value) => value.contains(text)
    Object(fields) => {
      let mut found = false
      for key, child in fields {
        if key.contains(text) || json_contains_text(child, text) {
          found = true
        }
      }
      found
    }
    Array(items) => items.any(fn(child) { json_contains_text(child, text) })
    _ => false
  }
}

///|
fn json_projection_value(value : Json, fields : Array[String]) -> Json {
  match value {
    Object(values) => {
      let output : Map[String, Json] = Map([])
      for field in fields {
        match values.get(field) {
          Some(child) => output[field] = child
          None => ()
        }
      }
      Json::object(output)
    }
    _ => value
  }
}

///|
pub fn resource_projection(value : Json, fields : Array[String]) -> Json {
  json_projection_value(value, fields)
}

///|
pub fn json_without(value : Json, fields : Array[String]) -> Json {
  match value {
    Object(values) => {
      let output : Map[String, Json] = Map([])
      for key, child in values {
        if !fields.contains(key) {
          output[key] = json_without(child, fields)
        }
      }
      Json::object(output)
    }
    Array(items) =>
      Json::array(items.map(fn(child) { json_without(child, fields) }))
    _ => value
  }
}

///|
pub fn json_merge(left : Json, right : Json) -> Json {
  match left {
    Object(left_fields) =>
      match right {
        Object(right_fields) => {
          let output : Map[String, Json] = Map([])
          for key, child in left_fields {
            output[key] = child
          }
          for key, child in right_fields {
            match output.get(key) {
              Some(existing) => output[key] = json_merge(existing, child)
              None => output[key] = child
            }
          }
          Json::object(output)
        }
        right_value => right_value
      }
    _ => right
  }
}

///|
fn json_leaf_paths_at(
  value : Json,
  prefix : String,
  output : Array[String],
) -> Unit {
  match value {
    Object(fields) =>
      if fields.length() == 0 {
        output.push(prefix)
      } else {
        for key, child in fields {
          let next = if prefix == "" { key } else { "\{prefix}.\{key}" }
          json_leaf_paths_at(child, next, output)
        }
      }
    Array(items) =>
      if items.length() == 0 {
        output.push(prefix)
      } else {
        for index, child in items {
          json_leaf_paths_at(child, "\{prefix}[\{index}]", output)
        }
      }
    _ => output.push(prefix)
  }
}

///|
pub fn json_leaf_paths(value : Json) -> Array[String] {
  let output : Array[String] = []
  json_leaf_paths_at(value, "", output)
  output
}

///|
pub fn json_flatten_scalars(value : Json) -> Array[String] {
  match value {
    Object(fields) => {
      let output : Array[String] = []
      for _, child in fields {
        output.append(json_flatten_scalars(child)[:])
      }
      output
    }
    Array(items) => {
      let output : Array[String] = []
      for child in items {
        output.append(json_flatten_scalars(child)[:])
      }
      output
    }
    _ =>
      match as_scalar_text(value) {
        Some(text) => [text]
        None => []
      }
  }
}

///|
pub fn json_equal_canonical(left : Json, right : Json) -> Bool {
  Json::equal(canonical_resource(left), canonical_resource(right))
}

///|
pub fn json_depth_at_least(value : Json, minimum : Int) -> Bool {
  json_depth(value) >= minimum
}

///|
pub fn resource_field_names(value : Json) -> Array[String] {
  json_object_keys(value)
}

///|
pub fn resource_has_field(value : Json, field : String) -> Bool {
  json_has_key(value, field)
}