///|
fn number_value(value : Json) -> Double? {
  match value {
    Number(number, ..) => Some(number)
    _ => None
  }
}

///|
pub fn number_at(value : Json, path : String) -> Double? {
  match query_first(value, path) {
    Some(item) => number_value(item)
    None => None
  }
}

///|
pub fn quantity_is_reasonable(
  value : Json,
  path : String,
  minimum : Double,
  maximum : Double,
) -> Bool {
  match number_at(value, "\{path}.value") {
    Some(number) => number >= minimum && number <= maximum
    None => false
  }
}

///|
pub fn quantity_unit(value : Json, path : String) -> String? {
  query_first(value, "\{path}.unit").map(fn(item) {
    as_scalar_text(item).unwrap_or("")
  })
}

///|
pub fn quantity_system(value : Json, path : String) -> String? {
  field_string(
    query_first(value, path).unwrap_or(Json::empty_object()),
    "system",
  )
}

///|
pub fn reference_is_well_formed(reference : String) -> Bool {
  if reference == "" || reference.contains(" ") {
    false
  } else if reference.has_prefix("http://") ||
    reference.has_prefix("https://") ||
    reference.has_prefix("urn:") {
    true
  } else {
    match reference.find("/") {
      Some(index) => index > 0 && index < reference.length() - 1
      None => false
    }
  }
}

///|
pub fn reference_resource_type(reference : String) -> String? {
  if reference_is_well_formed(reference) {
    match reference.find("/") {
      Some(index) => Some(reference[0:index].to_owned())
      None => None
    }
  } else {
    None
  }
}

///|
pub fn reference_resource_id(reference : String) -> String? {
  if reference_is_well_formed(reference) {
    match reference.rev_find("/") {
      Some(index) => Some(reference[index + 1:].to_owned())
      None => None
    }
  } else {
    None
  }
}

///|
pub fn validate_reference(value : Json, path : String) -> Array[Issue] {
  let output : Array[Issue] = []
  for index, reference in query_strings(value, "\{path}[*].reference") {
    if !reference_is_well_formed(reference) {
      output.push(
        issue(
          "\{path}/\{index}/reference",
          "value",
          "reference is not well formed",
        ),
      )
    }
  }
  let direct = query_text(value, "\{path}.reference", fallback="")
  if direct != "" && !reference_is_well_formed(direct) {
    output.push(
      issue("/\{path}/reference", "value", "reference is not well formed"),
    )
  }
  output
}

///|
pub fn validate_date_field(
  value : Json,
  path : String,
  datetime? : Bool = true,
) -> Array[Issue] {
  let output : Array[Issue] = []
  let text = query_text(value, path, fallback="")
  if text != "" {
    let valid = if datetime {
      is_valid_fhir_datetime(text)
    } else {
      is_valid_fhir_date(text)
    }
    if !valid {
      output.push(
        issue("/\{path}", "value", "date value is not a valid FHIR date"),
      )
    }
  }
  output
}

///|
pub fn validate_period(value : Json, path : String) -> Array[Issue] {
  let output : Array[Issue] = []
  let start = query_text(value, "\{path}.start", fallback="")
  let end = query_text(value, "\{path}.end", fallback="")
  if !period_is_valid(start, end) {
    output.push(
      issue("/\{path}", "value", "period boundaries are invalid or reversed"),
    )
  }
  output
}

///|
pub fn validate_identifier_array(
  value : Json,
  path? : String = "identifier",
) -> Array[Issue] {
  let output : Array[Issue] = []
  match query_first(value, path) {
    None => ()
    Some(Array(items)) =>
      for index, item in items {
        match field_string(item, "value") {
          Some(text) =>
            if text == "" {
              output.push(
                issue(
                  "/\{path}/\{index}/value",
                  "empty",
                  "identifier.value must not be empty",
                ),
              )
            }
          None =>
            output.push(
              issue(
                "/\{path}/\{index}/value",
                "required",
                "identifier.value is required",
              ),
            )
        }
        match field_string(item, "system") {
          Some(system) =>
            if system.contains(" ") {
              output.push(
                issue(
                  "/\{path}/\{index}/system",
                  "value",
                  "identifier.system must not contain spaces",
                ),
              )
            }
          None => ()
        }
      }
    Some(_) =>
      output.push(issue("/\{path}", "type", "identifier must be an array"))
  }
  output
}

///|
pub fn validate_coding_array(value : Json, path : String) -> Array[Issue] {
  let output : Array[Issue] = []
  match query_first(value, path) {
    None => ()
    Some(Array(items)) =>
      for index, item in items {
        match field_string(item, "code") {
          Some(code) =>
            if code == "" {
              output.push(
                issue(
                  "/\{path}/\{index}/code",
                  "empty",
                  "coding.code must not be empty",
                ),
              )
            }
          None =>
            output.push(
              issue(
                "/\{path}/\{index}/code",
                "required",
                "coding.code is required",
              ),
            )
        }
      }
    Some(_) => output.push(issue("/\{path}", "type", "coding must be an array"))
  }
  output
}

///|
pub fn validate_reference_fields(
  value : Json,
  paths : Array[String],
) -> Array[Issue] {
  let output : Array[Issue] = []
  for path in paths {
    output.append(validate_reference(value, path)[:])
  }
  output
}

///|
pub fn validate_date_fields(
  value : Json,
  paths : Array[String],
) -> Array[Issue] {
  let output : Array[Issue] = []
  for path in paths {
    output.append(validate_date_field(value, path)[:])
  }
  output
}

///|
pub fn resource_quality_issues(value : Json) -> Array[Issue] {
  let output : Array[Issue] = []
  output.append(validate_identifier_array(value)[:])
  output.append(
    validate_reference_fields(value, ["subject", "patient", "encounter"])[:],
  )
  output.append(
    validate_date_fields(value, [
      "birthDate", "issued", "performedDateTime", "occurrenceDateTime",
    ])[:],
  )
  output
}

///|
pub fn resource_quality_report(value : Json) -> ValidationReport {
  let report = validate_resource(value)
  report.issues.append(resource_quality_issues(value)[:])
  report
}

///|
pub fn resource_quality_is_valid(value : Json) -> Bool {
  !resource_quality_report(value).issues.any(fn(item) {
    item.severity == "error"
  })
}

///|
pub fn resource_issue_codes(value : Json) -> Array[String] {
  resource_quality_report(value).issues.map(fn(item) { item.code })
}