///|
pub suberror TopoError {
  Invalid(String)
} derive(Debug)

///|
pub struct Topology {
  priv data : Json
} derive(Debug)

///|
fn field(j : Json, key : String) -> Json {
  match j {
    Object(m) => m.get(key).unwrap_or(Json::null())
    _ => Json::null()
  }
}

///|
fn array(j : Json) -> Array[Json] {
  match j {
    Array(a) => a
    _ => []
  }
}

///|
fn number(j : Json) -> Double {
  match j {
    Number(n, ..) => n
    _ => 0.0
  }
}

///|
fn kind(j : Json) -> String {
  match field(j, "type") {
    String(s) => s
    _ => ""
  }
}

///|
fn finite(n : Double) -> Bool {
  !n.is_nan() && !n.is_inf()
}

///|
fn check_position(j : Json) -> Unit raise TopoError {
  guard j is Array(a) else { raise Invalid("coordinate.array") }
  if a.length() < 2 || a.length() > 8 {
    raise Invalid("coordinate.dimension")
  }
  for v in a {
    guard v is Number(n, ..) else { raise Invalid("coordinate.number") }
    if !finite(n) || n.abs() > 1.0e12 {
      raise Invalid("coordinate.range")
    }
  }
}

///|
fn check_refs(j : Json, depth : Int, count : Int) -> Unit raise TopoError {
  guard j is Array(a) else { raise Invalid("arcs.array") }
  for v in a {
    if depth > 1 {
      check_refs(v, depth - 1, count)
    } else {
      guard v is Number(n, ..) else { raise Invalid("arc.integer") }
      if !finite(n) ||
        n < -count.to_double() ||
        n >= count.to_double() ||
        n != n.to_int().to_double() {
        raise Invalid("arc.index")
      }
    }
  }
}

///|
fn check_geometry(g : Json, count : Int, depth : Int) -> Unit raise TopoError {
  if depth > 16 {
    raise Invalid("geometry.depth")
  }
  guard g is Object(_) else { raise Invalid("geometry.object") }
  match field(g, "type") {
    Null => ()
    String("Point") => check_position(field(g, "coordinates"))
    String("MultiPoint") => {
      guard field(g, "coordinates") is Array(a) else {
        raise Invalid("coordinates.array")
      }
      for p in a {
        check_position(p)
      }
    }
    String("LineString") => check_refs(field(g, "arcs"), 1, count)
    String("MultiLineString") | String("Polygon") =>
      check_refs(field(g, "arcs"), 2, count)
    String("MultiPolygon") => check_refs(field(g, "arcs"), 3, count)
    String("GeometryCollection") => {
      guard field(g, "geometries") is Array(a) else {
        raise Invalid("geometries.array")
      }
      for child in a {
        check_geometry(child, count, depth + 1)
      }
    }
    _ => raise Invalid("geometry.type")
  }
}

///|
fn validate(data : Json) -> Unit raise TopoError {
  if kind(data) != "Topology" {
    raise Invalid("topology.type")
  }
  guard field(data, "arcs") is Array(arcs) else {
    raise Invalid("topology.arcs")
  }
  if arcs.length() > 100000 {
    raise Invalid("limit.arcs")
  }
  let mut points = 0
  for arc in arcs {
    guard arc is Array(ps) else { raise Invalid("arc.array") }
    if ps.length() < 2 {
      raise Invalid("arc.minimum")
    }
    points += ps.length()
    if points > 1000000 {
      raise Invalid("limit.points")
    }
    for p in ps {
      check_position(p)
    }
  }
  guard field(data, "objects") is Object(objects) else {
    raise Invalid("topology.objects")
  }
  for _, g in objects {
    check_geometry(g, arcs.length(), 0)
  }
  check_expansion(data)
  let transform = field(data, "transform")
  if transform != Json::null() {
    for key in ["scale", "translate"] {
      check_position(field(transform, key))
      if array(field(transform, key)).length() != 2 {
        raise Invalid("transform.dimension")
      }
    }
    for s in array(field(transform, "scale")) {
      if number(s) == 0.0 {
        raise Invalid("transform.scale")
      }
    }
  }
}

///|
/// Parse a TopoJSON document. Malformed shapes and invalid arc references raise TopoError.
pub fn read(text : String) -> Topology raise TopoError {
  if text.length() > 16777216 {
    raise Invalid("limit.text")
  }
  let mut depth = 0
  let mut quoted = false
  let mut escaped = false
  for c in text.iter() {
    if quoted {
      if escaped {
        escaped = false
      } else if c == '\\' {
        escaped = true
      } else if c == '"' {
        quoted = false
      }
    } else if c == '"' {
      quoted = true
    } else if c == '{' || c == '[' {
      depth += 1
      if depth > 32 {
        raise Invalid("limit.depth")
      }
    } else if c == '}' || c == ']' {
      depth -= 1
    }
  }
  let data = @json.parse(text) catch { _ => raise Invalid("json.syntax") }
  validate(data)
  { data, }
}

///|
pub fn Topology::write(self : Topology) -> String {
  self.data.stringify()
}

///|
pub fn Topology::names(self : Topology) -> Array[String] {
  let names = match field(self.data, "objects") {
    Object(m) => m.keys().collect()
    _ => []
  }
  names.sort()
  names
}

///|
fn Topology::lookup(self : Topology, name : String) -> Json raise TopoError {
  match field(self.data, "objects") {
    Object(m) =>
      match m.get(name) {
        Some(g) => g
        None => raise Invalid("object.missing:" + name)
      }
    _ => raise Invalid("topology.objects")
  }
}

///|
/// Returns a detached topology geometry object (arc references are retained).
pub fn Topology::object(self : Topology, name : String) -> Json raise TopoError {
  let g = self.lookup(name)
  @json.parse(g.stringify()) catch {
    _ => raise Invalid("json.copy")
  }
}