///|
fn canonical_value(value : Json) -> Json {
  match value {
    Object(fields) => {
      let pairs = fields.to_array()
      pairs.sort_by(fn(left, right) { String::compare(left.0, right.0) })
      let ordered : Map[String, Json] = Map([])
      for _, pair in pairs {
        let key = pair.0
        let child = pair.1
        ordered[key] = canonical_value(child)
      }
      Json::object(ordered)
    }
    Array(items) => Json::array(items.map(canonical_value))
    _ => value
  }
}

///|
pub fn canonical_json(value : Json, indent? : Int = 0) -> String {
  canonical_value(value).stringify(indent~)
}

///|
pub fn canonical_resource(value : Json) -> Json {
  canonical_value(value)
}

///|
fn redact_value(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] = redact_value(child, fields)
        }
      }
      Json::object(output)
    }
    Array(items) =>
      Json::array(items.map(fn(item) { redact_value(item, fields) }))
    _ => value
  }
}

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

///|
pub fn redact_standard_sensitive_fields(value : Json) -> Json {
  redact_fields(value, ["telecom", "address", "photo", "identifier"])
}

///|
pub fn csv_escape(text : String) -> String {
  if text.contains(",") ||
    text.contains("\"") ||
    text.contains("\n") ||
    text.contains("\r") {
    let escaped = text.replace_all(old="\"", new="\"\"")
    "\"\{escaped}\""
  } else {
    text
  }
}

///|
pub fn csv_row(values : Array[String]) -> String {
  values.map(csv_escape).join(",")
}

///|
pub fn observation_csv_row_safe(value : Json) -> String {
  let summary = observation_summary(value)
  csv_row([
    summary.id.unwrap_or(""),
    summary.status.unwrap_or(""),
    summary.code_text.unwrap_or(""),
    summary.subject.unwrap_or(""),
    summary.value.unwrap_or(""),
    summary.issued.unwrap_or(""),
  ])
}

///|
pub fn json_size(value : Json) -> Int {
  canonical_json(value).length()
}

///|
pub fn json_depth(value : Json) -> Int {
  match value {
    Object(fields) => {
      let mut max_depth = 0
      for _, child in fields {
        let depth = json_depth(child)
        if depth > max_depth {
          max_depth = depth
        }
      }
      max_depth + 1
    }
    Array(items) => {
      let mut max_depth = 0
      for child in items {
        let depth = json_depth(child)
        if depth > max_depth {
          max_depth = depth
        }
      }
      max_depth + 1
    }
    _ => 1
  }
}

///|
pub fn json_scalar_count(value : Json) -> Int {
  match value {
    Object(fields) => {
      let mut count = 0
      for _, child in fields {
        count += json_scalar_count(child)
      }
      count
    }
    Array(items) => {
      let mut count = 0
      for child in items {
        count += json_scalar_count(child)
      }
      count
    }
    _ => 1
  }
}