///|
fn json_index_key(
  text : String,
  wires : Array[WireComponent],
  key : Array[String],
) -> Unit raise SdmxError {
  if wires.is_empty() {
    if !text.is_empty() {
      raise Invalid(
        path="index key",
        reason="expected empty key for zero dimensions",
      )
    }
    return
  }
  let parts = text.split(":").collect()
  if parts.length() != wires.length() {
    raise Invalid(path="index key", reason="wrong dimension count")
  }
  for i, wire in wires {
    let part = parts[i]
    if part.is_empty() || part.length() > 7 {
      raise Invalid(path="index", reason="invalid index length")
    }
    let mut n = 0
    for ch in part {
      if ch < '0' || ch > '9' {
        raise Invalid(path="index", reason="expected unsigned decimal index")
      }
      n = n * 10 + ch.to_int() - 48
    }
    let values = wire.values.unwrap()
    if n >= values.length() {
      raise Invalid(
        path=wire.component.id,
        reason="dictionary index out of range",
      )
    }
    key[wire.position] = values[n]
  }
}

///|
fn json_component_value(
  value : Json,
  wire : WireComponent,
) -> String? raise SdmxError {
  if value is Null {
    return wire.fallback
  }
  match wire.values {
    Some(values) => {
      if values.is_empty() {
        raise Invalid(
          path=wire.component.id,
          reason="index into empty dictionary",
        )
      }
      Some(values[j_int(value, wire.component.id, values.length() - 1)])
    }
    None => j_scalar(value, wire.component.id)
  }
}

///|
fn json_attribute_values(
  object : Map[String, Json],
  wires : Array[WireComponent],
) -> Array[String?] raise SdmxError {
  let values = match object.get("attributes") {
    None | Some(Null) => []
    Some(v) => j_array(v, "attributes")
  }
  if values.length() > wires.length() {
    raise Invalid(path="attributes", reason="too many values")
  }
  wires.mapi((i, wire) => {
    if i < values.length() {
      json_component_value(values[i], wire)
    } else {
      wire.fallback
    }
  })
}

///|
fn json_note_indices(
  object : Map[String, Json],
  notes : Array[Annotation],
) -> Array[Int32] raise SdmxError {
  if object.get("dimensionGroupAttributes") is Some(value) &&
    !j_object(value, "dimensionGroupAttributes").is_empty() {
    raise Unsupported(feature="dimensionGroupAttributes")
  }
  let values = json_level(object, "annotations")
  if values.length() > 1024 {
    raise Limit(resource="annotation references", maximum=1024)
  }
  values.map(value => {
    if notes.is_empty() {
      raise Invalid(path="annotation index", reason="no annotation dictionary")
    }
    j_int(value, "annotation index", notes.length() - 1)
  })
}

///|
fn merge_note_indices(
  left : Array[Int32],
  right : Array[Int32],
) -> Array[Int32] raise SdmxError {
  let result : Array[Int32] = []
  let seen : Map[Int32, Bool] = Map([])
  for group in [left, right] {
    for i in group {
      if !seen.contains(i) {
        seen[i] = true
        result.push(i)
      }
    }
  }
  if result.length() > 1024 {
    raise Limit(resource="annotations per observation", maximum=1024)
  }
  result
}

///|
fn json_observations(
  object : Map[String, Json],
  layout : JsonLayout,
  prefix : Array[String],
  inherited : Array[String?],
  rows : Array[Observation],
  inherited_notes : Array[Int32],
  bindings : Array[(Array[String], Array[Int32])],
  references : Array[Int32],
  limits : Limits,
) -> Unit raise SdmxError {
  let observations = match object.get("observations") {
    None | Some(Null) => Map([])
    Some(value) => j_object(value, "observations")
  }
  for encoded_key, value in observations {
    if rows.length() >= limits.max_rows {
      raise Limit(resource="observations", maximum=limits.max_rows)
    }
    let key = prefix.copy()
    json_index_key(encoded_key, layout.observation_dimensions, key)
    let tuple = j_array(value, "observation tuple")
    let measure_count = layout.measures.length()
    if tuple.length() < measure_count {
      raise Invalid(path=encoded_key, reason="missing measure positions")
    }
    let note_start = measure_count + layout.observation_attributes.length()
    let extra : Array[Int32] = []
    for i = note_start; i < tuple.length(); i = i + 1 {
      if layout.notes.is_empty() {
        raise Invalid(path="annotation", reason="no annotation dictionary")
      }
      extra.push(j_int(tuple[i], "annotation index", layout.notes.length() - 1))
      if extra.length() > 1024 {
        raise Limit(resource="annotation references", maximum=1024)
      }
    }
    let row_notes = merge_note_indices(inherited_notes, extra)
    references[0] += row_notes.length()
    if references[0] > 1000000 {
      raise Limit(resource="annotation references", maximum=1000000)
    }
    if !row_notes.is_empty() {
      bindings.push((key.copy(), row_notes))
    }
    let measures = layout.measures.mapi((i, wire) => {
      json_component_value(tuple[i], wire)
    })
    let attributes = inherited.copy()
    for i, wire in layout.observation_attributes {
      let position = measure_count + i
      attributes.push(
        if position < tuple.length() {
          json_component_value(tuple[position], wire)
        } else {
          wire.fallback
        },
      )
    }
    rows.push(Observation::new(layout.structure, key, measures, attributes))
  }
}

///|
pub fn decode_json2(
  text : String,
  expected~ : Structure? = None,
  limits~ : Limits = Limits::standard(),
) -> DataMessage raise SdmxError {
  let root = j_object(parse_bounded_json(text, limits), "message")
  if root.contains("errors") {
    raise Unsupported(feature="SDMX error message")
  }
  let meta = j_object(j_required(root, "meta"), "meta")
  ignore(j_string(j_required(meta, "id"), "meta.id"))
  ignore(j_object(j_required(meta, "sender"), "meta.sender"))
  if meta.get("schema") is Some(value) {
    let schema = j_string(value, "meta.schema")
    if !schema.contains("/2.0") {
      raise Unsupported(feature="JSON message version other than 2.0")
    }
  }
  let data = j_object(j_required(root, "data"), "data")
  let structures = j_array(j_required(data, "structures"), "structures")
  if structures.length() != 1 {
    raise Unsupported(feature="multiple or absent embedded structures")
  }
  let layout = json_layout(structures[0], expected, limits)
  let datasets = j_array(j_required(data, "dataSets"), "dataSets")
  if datasets.length() != 1 {
    raise Unsupported(feature="multiple or absent data sets")
  }
  let object = j_object(datasets[0], "dataSet")
  if object.get("structure") is Some(value) &&
    j_int(value, "dataSet.structure", 0) != 0 {
    raise Invalid(path="dataSet.structure", reason="index out of range")
  }
  if j_optional_string(object, "action", "Information") != "Information" {
    raise Unsupported(
      feature="JSON mutating action: only Information is accepted",
    )
  }
  let dataset_notes = json_note_indices(object, layout.notes)
  let prefix = Array::make(layout.structure.dimensions.length(), "")
  for wire in layout.dataset_dimensions {
    prefix[wire.position] = wire.values.unwrap()[0]
  }
  let inherited = json_attribute_values(object, layout.dataset_attributes)
  let rows : Array[Observation] = []
  let bindings : Array[(Array[String], Array[Int32])] = []
  let references = [0]
  match object.get("series") {
    Some(value) if !(value is Null) => {
      if object.contains("observations") {
        raise Invalid(
          path="dataSet",
          reason="series and flat observations cannot coexist",
        )
      }
      let series = j_object(value, "series")
      if series.length() > limits.max_rows {
        raise Limit(resource="series", maximum=limits.max_rows)
      }
      for encoded_key, value in series {
        let key = prefix.copy()
        json_index_key(encoded_key, layout.series_dimensions, key)
        let object = j_object(value, "series")
        let notes = merge_note_indices(
          dataset_notes,
          json_note_indices(object, layout.notes),
        )
        let attributes = inherited.copy()
        for v in json_attribute_values(object, layout.series_attributes) {
          attributes.push(v)
        }
        json_observations(
          object, layout, key, attributes, rows, notes, bindings, references, limits,
        )
      }
    }
    _ => {
      if !layout.series_dimensions.is_empty() ||
        !layout.series_attributes.is_empty() {
        raise Invalid(
          path="dataSet",
          reason="series components require series data layout",
        )
      }
      json_observations(
        object, layout, prefix, inherited, rows, dataset_notes, bindings, references,
        limits,
      )
    }
  }
  let dataset = Dataset::new(layout.structure, rows)
  let validated = match expected {
    None => dataset
    Some(s) => dataset.validate_with(s)
  }
  DataMessage::new(layout.kind, layout.reference, validated).with_annotations(
    layout.notes,
    bindings,
  )
}