///|
pub struct PayloadField {
  key : String
  value : String
} derive(Eq, Debug)

///|
pub fn PayloadField::new(key : String, value : String) -> PayloadField {
  { key, value }
}

///|
pub(all) enum PayloadKind {
  ObjectPayload
  ArrayPayload
  ScalarPayload
  InvalidPayload
} derive(Eq, Debug)

///|
pub struct PayloadDocument {
  body : String
  json : Json?
  kind : PayloadKind
  error : String
} derive(Eq, Debug)

///|
pub fn PayloadDocument::invalid(
  body : String,
  error : String,
) -> PayloadDocument {
  { body, json: None, kind: InvalidPayload, error }
}

///|
pub fn PayloadDocument::valid(body : String, json : Json) -> PayloadDocument {
  let kind = match json {
    Object(_) => ObjectPayload
    Array(_) => ArrayPayload
    _ => ScalarPayload
  }
  { body, json: Some(json), kind, error: "" }
}

///|
pub fn parse_payload_document(body : String) -> PayloadDocument {
  PayloadDocument::valid(body, @json.parse(body)) catch {
    error => PayloadDocument::invalid(body, error.to_string())
  }
}

///|
pub fn payload_is_valid_json(body : String) -> Bool {
  parse_payload_document(body).json is Some(_)
}

///|
fn json_scalar_text(value : Json) -> String {
  match value {
    String(text) => text
    True => "true"
    False => "false"
    Null => "null"
    Number(_, ..) => value.stringify()
    Array(_) | Object(_) => value.stringify()
  }
}

///|
pub fn json_path_value(json : Json, path : Array[String]) -> Json? {
  let mut current = json
  for segment in path {
    match current {
      Object(fields) =>
        match fields.get(segment) {
          Some(value) => current = value
          None => return None
        }
      _ => return None
    }
  }
  Some(current)
}

///|
pub fn json_path_text(json : Json, path : Array[String]) -> String? {
  match json_path_value(json, path) {
    Some(value) => Some(json_scalar_text(value))
    None => None
  }
}

///|
pub fn payload_path_text(body : String, path : Array[String]) -> String? {
  let document = parse_payload_document(body)
  match document.json {
    Some(json) => json_path_text(json, path)
    None => None
  }
}

///|
pub fn payload_first_path(
  body : String,
  paths : Array[Array[String]],
) -> String? {
  let document = parse_payload_document(body)
  let json = match document.json {
    Some(value) => value
    None => return None
  }
  for path in paths {
    match json_path_text(json, path) {
      Some(value) => return Some(value)
      None => ()
    }
  }
  None
}

///|
pub fn parse_payload_fields(body : String) -> Array[PayloadField] {
  let document = parse_payload_document(body)
  match document.json {
    Some(Object(fields)) => {
      let result : Array[PayloadField] = []
      for key, value in fields {
        result.push(PayloadField::new(key, json_scalar_text(value)))
      }
      result
    }
    _ => []
  }
}

///|
pub fn payload_value(fields : Array[PayloadField], key : String) -> String? {
  for field in fields {
    if field.key == key {
      return Some(field.value)
    }
  }
  None
}

///|
pub fn payload_has_key(fields : Array[PayloadField], key : String) -> Bool {
  payload_value(fields, key) is Some(_)
}

///|
pub fn payload_summary(fields : Array[PayloadField]) -> String {
  let builder = StringBuilder::new()
  for i in 0.. 0 {
      builder.write_string(",")
    }
    builder.write_string(fields[i].key)
    builder.write_string("=")
    builder.write_string(fields[i].value)
  }
  builder.to_string()
}

///|
pub fn payload_keys(body : String) -> Array[String] {
  let fields = parse_payload_fields(body)
  Array::makei(fields.length(), index => fields[index].key)
}

///|
pub fn payload_contains_path(body : String, path : Array[String]) -> Bool {
  payload_path_text(body, path) is Some(_)
}

///|
pub fn payload_document_summary(document : PayloadDocument) -> String {
  match document.kind {
    InvalidPayload => "invalid: " + document.error
    ObjectPayload => {
      let count = parse_payload_fields(document.body).length()
      "object fields=" + count.to_string()
    }
    ArrayPayload =>
      match document.json {
        Some(Array(items)) => "array items=" + items.length().to_string()
        _ => "array items=0"
      }
    ScalarPayload => "scalar"
  }
}