// Reading and writing scenegraphs as JSON, in the same shape as
// vega-scenegraph's `sceneToJSON` (marks with `marktype` and `items`; group
// items carry their child marks in `items`).

///|
fn json_field(m : Map[String, Json], k : String) -> Json? {
  m.get(k)
}

///|
/// Build a mark (and everything below it) from JSON.
pub fn mark_from_json(j : Json) -> Mark raise SceneError {
  let m = match j {
    Object(m) => m
    _ => raise SceneError("scene mark must be an object")
  }
  let typename = match json_field(m, "marktype") {
    Some(String(s)) => s
    _ => raise SceneError("scene mark has no marktype")
  }
  let marktype = match MarkType::from_name(typename) {
    Some(t) => t
    None => raise SceneError("unsupported mark type: " + typename)
  }
  let name = match json_field(m, "name") {
    Some(String(s)) => Some(s)
    _ => None
  }
  let role = match json_field(m, "role") {
    Some(String(s)) => Some(s)
    _ => None
  }
  let interactive = match json_field(m, "interactive") {
    Some(False) => false
    _ => true
  }
  let clip = match json_field(m, "clip") {
    Some(True) => true
    _ => false
  }
  let zindex = match json_field(m, "zindex") {
    Some(Number(x, ..)) => Some(x.to_int())
    _ => None
  }
  let aria = match json_field(m, "aria") {
    Some(False) => false
    _ => true
  }
  let items : Array[Item] = []
  match json_field(m, "items") {
    Some(Array(list)) =>
      for it in list {
        match it {
          Object(o) => {
            let item = Item::new()
            for k, v in o {
              if k == "items" && marktype is Group {
                match v {
                  Array(children) =>
                    for c in children {
                      item.children.push(mark_from_json(c))
                    }
                  _ => ()
                }
              } else {
                item.props[k] = @value.from_json(v)
              }
            }
            items.push(item)
          }
          _ => raise SceneError("scene item must be an object")
        }
      }
    _ => ()
  }
  Mark::new(marktype, name?, role?, interactive~, clip~, zindex?, aria~, items~)
}

///|
fn value_to_json(v : @value.Value) -> Json {
  match v {
    Null | Undef => Json::null()
    Bool(b) => Json::boolean(b)
    Num(x) =>
      if x.is_nan() || x.is_inf() {
        Json::null()
      } else {
        Json::number(x)
      }
    Str(s) => Json::string(s)
    Arr(items) => Json::array(items.map(value_to_json))
    Obj(m) => {
      let o : Map[String, Json] = Map([])
      for k, x in m {
        o[k] = value_to_json(x)
      }
      Json::object(o)
    }
  }
}

///|
fn bounds_to_json(b : Bounds) -> Json {
  let o : Map[String, Json] = Map([])
  o["x1"] = Json::number(b.x1)
  o["y1"] = Json::number(b.y1)
  o["x2"] = Json::number(b.x2)
  o["y2"] = Json::number(b.y2)
  Json::object(o)
}

///|
/// Serialise a mark. With `with_bounds`, every mark and item carries its
/// bounding box under `bounds`.
pub fn mark_to_json(mark : Mark, with_bounds~ : Bool = false) -> Json {
  let o : Map[String, Json] = Map([])
  o["marktype"] = Json::string(mark.marktype.name())
  match mark.name {
    Some(n) => o["name"] = Json::string(n)
    None => ()
  }
  match mark.role {
    Some(r) => o["role"] = Json::string(r)
    None => ()
  }
  o["interactive"] = Json::boolean(mark.interactive)
  o["clip"] = Json::boolean(mark.clip)
  match mark.zindex {
    Some(z) => o["zindex"] = Json::number(z.to_double())
    None => ()
  }
  if !mark.aria {
    o["aria"] = Json::boolean(false)
  }
  if with_bounds {
    o["bounds"] = bounds_to_json(mark.bounds)
  }
  let items : Array[Json] = []
  for item in mark.items {
    let io : Map[String, Json] = Map([])
    for k, v in item.props {
      io[k] = value_to_json(v)
    }
    if with_bounds && !mark.marktype.is_nested() {
      io["bounds"] = bounds_to_json(item.bounds)
    }
    if mark.marktype is Group {
      io["items"] = Json::array(
        item.children.map(fn(c) { mark_to_json(c, with_bounds~) }),
      )
    }
    items.push(Json::object(io))
  }
  o["items"] = Json::array(items)
  Json::object(o)
}