///|
/// One STIX object, including custom `x-*` types.
pub(all) struct StixObject {
  stix_type : String
  id : String
  spec_version : String
  kind : ObjectKind
  properties : Map[String, Json]
  raw : Json
} derive(Debug)

///|
/// Bundle of STIX objects.
pub(all) struct StixBundle {
  id : String
  objects : Array[StixObject]
  raw : Json
} derive(Debug)

///|
/// A document is either a bundle or a single object.
pub(all) enum Document {
  Bundle(StixBundle)
  Object(StixObject)
} derive(Debug)

///|
pub fn parse_document(text : String) -> Result[Document, StixError] {
  let json = match parse_json_text(text) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  parse_document_json(json)
}

///|
pub fn parse_document_json(value : Json) -> Result[Document, StixError] {
  let fields = match as_object(value) {
    Some(map) => map
    None => return Err(NotObject)
  }
  match object_string(fields, "type") {
    Some("bundle") =>
      match parse_bundle(value, fields) {
        Ok(bundle) => Ok(Bundle(bundle))
        Err(err) => Err(err)
      }
    Some(_) =>
      match parse_stix_object(value, "") {
        Ok(obj) => Ok(Object(obj))
        Err(err) => Err(err)
      }
    None => Err(UnexpectedRoot("missing type"))
  }
}

///|
fn parse_bundle(
  raw : Json,
  fields : Map[String, Json],
) -> Result[StixBundle, StixError] {
  let id = match object_string(fields, "id") {
    Some(text) => text
    None => ""
  }
  let objects : Array[StixObject] = []
  match object_field(fields, "objects") {
    None => ()
    Some(Null) => ()
    Some(value) =>
      match as_array(value) {
        None => return Err(UnexpectedRoot("bundle.objects must be an array"))
        Some(items) =>
          for i = 0; i < items.length(); i = i + 1 {
            match parse_stix_object(items[i], index_path("objects", i)) {
              Ok(obj) => objects.push(obj)
              Err(err) => return Err(err)
            }
          }
      }
  }
  Ok({ id, objects, raw, })
}

///|
fn parse_stix_object(
  raw : Json,
  _path : String,
) -> Result[StixObject, StixError] {
  let fields = match as_object(raw) {
    Some(map) => map
    None => return Err(NotObject)
  }
  let stix_type = match object_string(fields, "type") {
    Some(text) => text
    None => ""
  }
  let id = match object_string(fields, "id") {
    Some(text) => text
    None => ""
  }
  let spec_version = match object_string(fields, "spec_version") {
    Some(text) => text
    None => "2.1"
  }
  let kind = object_kind_of(stix_type)
  Ok({
    stix_type,
    id,
    spec_version,
    kind,
    properties: json_clone_object(fields),
    raw,
  })
}

///|
pub fn document_objects(doc : Document) -> Array[StixObject] {
  match doc {
    Bundle(bundle) => bundle.objects
    Object(obj) => [obj]
  }
}

///|
pub fn stringify_document(doc : Document) -> String {
  match doc {
    Bundle(bundle) => stringify_json(bundle.raw)
    Object(obj) => stringify_json(obj.raw)
  }
}