///|
fn json_string(value : Json) -> String? {
  match value {
    String(s) => Some(s)
    _ => None
  }
}

///|
fn json_bool(value : Json) -> Bool? {
  match value {
    True => Some(true)
    False => Some(false)
    _ => None
  }
}

///|
fn json_int(value : Json) -> Int? {
  match value {
    Number(n, ..) => Some(n.to_int())
    _ => None
  }
}

///|
fn json_object(value : Json) -> Map[String, Json]? {
  match value {
    Object(x) => Some(x)
    _ => None
  }
}

///|
fn json_array(value : Json) -> Array[Json]? {
  match value {
    Array(x) => Some(x)
    _ => None
  }
}

///|
fn parse_name_json(value : Json) -> CslName? {
  match json_object(value) {
    None => None
    Some(obj) => {
      let family = match obj.get("family") {
        Some(x) => json_string(x).unwrap_or("")
        None => ""
      }
      let given = match obj.get("given") {
        Some(x) => json_string(x).unwrap_or("")
        None => ""
      }
      let n = CslName::new(family, given~)
      n.suffix = match obj.get("suffix") {
        Some(x) => json_string(x).unwrap_or("")
        None => ""
      }
      n.dropping_particle = match obj.get("dropping-particle") {
        Some(x) => json_string(x).unwrap_or("")
        None => ""
      }
      n.non_dropping_particle = match obj.get("non-dropping-particle") {
        Some(x) => json_string(x).unwrap_or("")
        None => ""
      }
      Some(n)
    }
  }
}

///|
fn parse_names_json(value : Json) -> Array[CslName] {
  match json_array(value) {
    None => []
    Some(arr) => {
      let out = []
      for v in arr {
        match parse_name_json(v) {
          Some(n) => out.push(n)
          None => ()
        }
      }
      out
    }
  }
}

///|
fn parse_date_json(value : Json) -> CslDate? {
  match json_object(value) {
    None => None
    Some(obj) => {
      let date = CslDate::new(0)
      match obj.get("literal") {
        Some(v) => date.literal = json_string(v).unwrap_or("")
        None => ()
      }
      match obj.get("circa") {
        Some(v) => date.circa = json_bool(v).unwrap_or(false)
        None => ()
      }
      match obj.get("date-parts") {
        Some(v) =>
          match json_array(v) {
            Some(parts) =>
              match parts.get(0) {
                Some(v) =>
                  match json_array(v) {
                    Some(xs) => {
                      date.year = match xs.get(0) {
                        Some(x) => json_int(x).unwrap_or(0)
                        None => 0
                      }
                      date.month = match xs.get(1) {
                        Some(x) => json_int(x).unwrap_or(0)
                        None => 0
                      }
                      date.day = match xs.get(2) {
                        Some(x) => json_int(x).unwrap_or(0)
                        None => 0
                      }
                    }
                    None => ()
                  }
                _ => ()
              }
            None => ()
          }
        None => ()
      }
      Some(date)
    }
  }
}

///|
fn assign_json_field(item : Item, key : String, value : Json) -> Unit {
  match key {
    "id" =>
      match json_string(value) {
        Some(v) => item.id = v
        None => ()
      }
    "title" =>
      match json_string(value) {
        Some(v) => item.title = v
        None => ()
      }
    "container-title" =>
      match json_string(value) {
        Some(v) => item.container_title = v
        None => ()
      }
    "publisher" =>
      match json_string(value) {
        Some(v) => item.publisher = v
        None => ()
      }
    "publisher-place" =>
      match json_string(value) {
        Some(v) => item.publisher_place = v
        None => ()
      }
    "volume" =>
      match json_string(value) {
        Some(v) => item.volume = v
        None => ()
      }
    "issue" =>
      match json_string(value) {
        Some(v) => item.issue = v
        None => ()
      }
    "page" =>
      match json_string(value) {
        Some(v) => item.page = v
        None => ()
      }
    "edition" =>
      match json_string(value) {
        Some(v) => item.edition = v
        None => ()
      }
    "URL" =>
      match json_string(value) {
        Some(v) => item.url = v
        None => ()
      }
    "DOI" =>
      match json_string(value) {
        Some(v) => item.doi = v
        None => ()
      }
    "type" =>
      match json_string(value) {
        Some(v) => item.item_type = v
        None => ()
      }
    "author" | "editor" | "translator" | "container-author" =>
      item.names[key] = parse_names_json(value)
    "issued" | "accessed" | "event-date" | "submitted" =>
      match parse_date_json(value) {
        Some(v) => item.dates[key] = v
        None => ()
      }
    _ =>
      match json_string(value) {
        Some(v) => item.strings[key] = v
        None =>
          match json_int(value) {
            Some(v) => item.numbers[key] = v
            None =>
              match json_bool(value) {
                Some(v) => item.booleans[key] = v
                None => ()
              }
          }
      }
  }
}

///|
pub fn parse_item_json(value : Json) -> Result[Item, CslError] {
  match json_object(value) {
    None => Err(BadJson("item must be an object"))
    Some(obj) => {
      let id = match obj.get("id") {
        Some(x) => json_string(x).unwrap_or("")
        None => ""
      }
      if id.is_empty() {
        return Err(BadItem("item id is required"))
      }
      let item = Item::new(id)
      obj.each(fn(key, value) { assign_json_field(item, key, value) })
      Ok(item)
    }
  }
}

///|
pub fn parse_items_json(json_text : String) -> Result[Array[Item], CslError] {
  let value = @json.parse(json_text) catch {
    _ => return Err(BadJson("invalid JSON"))
  }
  match value {
    Array(values) => {
      let out = []
      for value in values {
        match parse_item_json(value) {
          Ok(item) => out.push(item)
          Err(e) => return Err(e)
        }
      }
      Ok(out)
    }
    Object(_) =>
      match parse_item_json(value) {
        Ok(item) => Ok([item])
        Err(e) => Err(e)
      }
    _ => Err(BadJson("top-level JSON must be an object or array"))
  }
}

///|
pub fn parse_cites_json(json_text : String) -> Result[Array[Cite], CslError] {
  let value = @json.parse(json_text) catch {
    _ => return Err(BadJson("invalid citation JSON"))
  }
  let values = match value {
    Array(x) => x
    Object(_) => [value]
    _ => return Err(BadJson("citations must be an array"))
  }
  let out = []
  for value in values {
    match json_object(value) {
      Some(obj) => {
        let id = match obj.get("id") {
          Some(v) => json_string(v).unwrap_or("")
          None => ""
        }
        if id.is_empty() {
          return Err(BadJson("citation id is required"))
        }
        let cite = Cite::new(id)
        match obj.get("locator") {
          Some(v) => cite.locator = json_string(v).unwrap_or("")
          None => ()
        }
        match obj.get("label") {
          Some(v) => cite.label = json_string(v).unwrap_or("page")
          None => ()
        }
        match obj.get("prefix") {
          Some(v) => cite.prefix = json_string(v).unwrap_or("")
          None => ()
        }
        match obj.get("suffix") {
          Some(v) => cite.suffix = json_string(v).unwrap_or("")
          None => ()
        }
        out.push(cite)
      }
      None => return Err(BadJson("citation must be an object"))
    }
  }
  Ok(out)
}

///|
pub fn Processor::render_bibliography_json(
  self : Processor,
  items_json : String,
  format? : String = "text",
) -> Result[String, CslError] {
  let items = match parse_items_json(items_json) {
    Ok(x) => x
    Err(e) => return Err(e)
  }
  self.render_bibliography(items, format~)
}

///|
pub fn Processor::render_citation_json(
  self : Processor,
  items_json : String,
  cites_json : String,
  format? : String = "text",
) -> Result[String, CslError] {
  let items = match parse_items_json(items_json) {
    Ok(x) => x
    Err(e) => return Err(e)
  }
  let cites = match parse_cites_json(cites_json) {
    Ok(x) => x
    Err(e) => return Err(e)
  }
  self.render_citation(items, cites, format~)
}