///|
pub struct Dataset {
  priv structure : Structure
  priv rows : Array[Observation]
  priv index : Map[String, Int32]
} derive(@debug.Debug)

///|
pub extend Dataset with @debug.Debug::{to_repr}

///|
pub fn Dataset::new(
  structure : Structure,
  rows : Array[Observation],
) -> Dataset raise SdmxError {
  if rows.length() > structure.limits.max_rows {
    raise Limit(resource="observations", maximum=structure.limits.max_rows)
  }
  let detached : Array[Observation] = []
  let mut text_units = 0
  let index : Map[String, Int32] = Map([])
  let attached : Array[Map[String, String?]] = structure.attributes.map(_ => {
    Map([])
  })
  for n, input in rows {
    let row = Observation::new(
      structure,
      input.key,
      input.measures,
      input.attributes,
    )
    for value in row.key {
      if text_units > structure.limits.max_text - value.length() {
        raise Limit(resource="dataset text", maximum=structure.limits.max_text)
      }
      text_units += value.length()
    }
    for group in [row.measures, row.attributes] {
      for value in group {
        if value is Some(text) {
          if text_units > structure.limits.max_text - text.length() {
            raise Limit(
              resource="dataset text",
              maximum=structure.limits.max_text,
            )
          }
          text_units += text.length()
        }
      }
    }
    let token = row.key_token()
    if index.contains(token) {
      raise Conflict(
        key=token,
        reason="duplicate observation key at row \{n + 1}",
      )
    }
    for i, attribute in structure.attributes {
      let scope = attachment_key(structure, attribute, row.key)
      match attached[i].get(scope) {
        Some(value) =>
          if value != row.attributes[i] {
            raise Conflict(
              key=token,
              reason="inconsistent attached attribute: \{attribute.id}",
            )
          }
        None => attached[i][scope] = row.attributes[i]
      }
    }
    index[token] = detached.length()
    detached.push(row)
  }
  { structure, rows: detached, index, }
}

///|
pub fn Dataset::structure(self : Dataset) -> Structure {
  self.structure
}

///|
pub fn Dataset::length(self : Dataset) -> Int32 {
  self.rows.length()
}

///|
pub fn Dataset::observations(self : Dataset) -> Array[Observation] {
  self.rows.map(detached_observation)
}

///|
pub fn Dataset::get(
  self : Dataset,
  key : Array[String],
) -> Observation? raise SdmxError {
  if key.length() != self.structure.dimensions.length() {
    raise Invalid(path="key", reason="wrong dimension count")
  }
  for i, component in self.structure.dimensions {
    validate_value(component, key[i], self.structure.limits)
  }
  match self.index.get(framed_key(key)) {
    Some(i) => Some(detached_observation(self.rows[i]))
    None => None
  }
}

///|
fn compare_keys(
  a : Observation,
  b : Observation,
  structure : Structure,
) -> Int32 {
  for i, component in structure.dimensions {
    let order = if component.role is TimeDimension {
      // Observation construction has already checked these period strings.
      let x = PeriodValue::parse(a.key[i]) catch {
        _ => abort("validated period")
      }
      let y = PeriodValue::parse(b.key[i]) catch {
        _ => abort("validated period")
      }
      let c = x.compare_period(y)
      if c == 0 {
        a.key[i].lexical_compare(b.key[i])
      } else {
        c
      }
    } else {
      a.key[i].lexical_compare(b.key[i])
    }
    if order != 0 {
      return order
    }
  }
  0
}

///|
pub fn Dataset::sorted(self : Dataset) -> Dataset raise SdmxError {
  let rows = self.observations()
  rows.sort_by((a, b) => compare_keys(a, b, self.structure))
  Dataset::new(self.structure, rows)
}

///|
pub fn Dataset::validate_with(
  self : Dataset,
  structure : Structure,
) -> Dataset raise SdmxError {
  let left = self.structure.components()
  let right = structure.components()
  if left.length() != right.length() {
    raise Invalid(path="structure", reason="different component count")
  }
  for i, c in left {
    if c.id != right[i].id {
      raise Invalid(path="structure", reason="component order differs")
    }
    let compatible = match (c.role, right[i].role) {
      (Dimension, Dimension)
      | (TimeDimension, TimeDimension)
      | (Measure, Measure) => true
      (Attribute(a), Attribute(b)) => a == b
      _ => false
    }
    if !compatible {
      raise Invalid(path=c.id, reason="component roles/attachments differ")
    }
  }
  if self.structure.reference != structure.reference {
    raise Invalid(path="structure", reason="reference differs")
  }
  Dataset::new(structure, self.rows)
}