///|
// Revisions compare lexical component values: omission and numeric zero differ.
pub struct FieldRevision {
  priv component : String
  priv before : String?
  priv after : String?
} derive(Eq, @debug.Debug)

///|
pub extend FieldRevision with Eq::{equal, not_equal}

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

///|
pub fn FieldRevision::component(self : FieldRevision) -> String {
  self.component
}

///|
pub fn FieldRevision::before(self : FieldRevision) -> String? {
  self.before
}

///|
pub fn FieldRevision::after(self : FieldRevision) -> String? {
  self.after
}

///|
pub struct ObservationRevision {
  priv key : Array[String]
  priv measures : Array[FieldRevision]
  priv attributes : Array[FieldRevision]
} derive(@debug.Debug)

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

///|
fn detached_revision(change : ObservationRevision) -> ObservationRevision {
  {
    key: change.key.copy(),
    measures: change.measures.copy(),
    attributes: change.attributes.copy(),
  }
}

///|
pub fn ObservationRevision::key(self : ObservationRevision) -> Array[String] {
  self.key.copy()
}

///|
pub fn ObservationRevision::measures(
  self : ObservationRevision,
) -> Array[FieldRevision] {
  self.measures.copy()
}

///|
pub fn ObservationRevision::attributes(
  self : ObservationRevision,
) -> Array[FieldRevision] {
  self.attributes.copy()
}

///|
pub struct DatasetRevision {
  priv structure_profile : String
  priv added : Array[Observation]
  priv removed : Array[Observation]
  priv changed : Array[ObservationRevision]
  priv limits : Limits
} derive(@debug.Debug)

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

///|
pub fn DatasetRevision::added(self : DatasetRevision) -> Array[Observation] {
  self.added.map(detached_observation)
}

///|
pub fn DatasetRevision::removed(self : DatasetRevision) -> Array[Observation] {
  self.removed.map(detached_observation)
}

///|
pub fn DatasetRevision::changed(
  self : DatasetRevision,
) -> Array[ObservationRevision] {
  self.changed.map(detached_revision)
}

///|
pub fn DatasetRevision::is_empty(self : DatasetRevision) -> Bool {
  self.added.is_empty() && self.removed.is_empty() && self.changed.is_empty()
}

///|
fn revision_structure_compatible(
  left : Structure,
  right : Structure,
) -> Unit raise SdmxError {
  if left.to_profile_json() != right.to_profile_json() {
    raise Invalid(
      path="revision.structure",
      reason="exact structure profile differs",
    )
  }
}

///|
fn optional_text_json(value : String?) -> Json {
  match value {
    None => Json::null()
    Some(text) => Json::string(text)
  }
}

///|
fn revision_row_json(row : Observation) -> Json {
  {
    "key": row.key.map(Json::string),
    "measures": row.measures.map(optional_text_json),
    "attributes": row.attributes.map(optional_text_json),
  }
}

///|
// The exact canonical snapshot is a collision-free local equality token,
// not a digest, signature, authentication credential or compact wire hash.
pub fn Dataset::snapshot_signature(self : Dataset) -> String raise SdmxError {
  let rows = self.observations()
  rows.sort_by((a, b) => a.key_token().lexical_compare(b.key_token()))
  canonical_json(
    {
      "profile": "moonsdmx.local-snapshot/1",
      "structure": self.structure.to_profile_json(),
      "rows": rows.map(revision_row_json),
    },
    self.structure.limits,
  )
}

///|
fn field_revisions(
  components : Array[Component],
  before : Array[String?],
  after : Array[String?],
) -> Array[FieldRevision] {
  let changes : Array[FieldRevision] = []
  for i, component in components {
    if before[i] != after[i] {
      changes.push({
        component: component.id,
        before: before[i],
        after: after[i],
      })
    }
  }
  changes
}

///|
pub fn Dataset::revisions_to(
  self : Dataset,
  next : Dataset,
) -> DatasetRevision raise SdmxError {
  revision_structure_compatible(self.structure, next.structure)
  let added : Array[Observation] = []
  let removed : Array[Observation] = []
  let changed : Array[ObservationRevision] = []
  let keys : Array[String] = self.index.keys().collect()
  keys.sort_by((a, b) => a.lexical_compare(b))
  for key in keys {
    let before = self.rows[self.index[key]]
    match next.index.get(key) {
      None => removed.push(detached_observation(before))
      Some(position) => {
        let after = next.rows[position]
        let measures = field_revisions(
          self.structure.measures,
          before.measures,
          after.measures,
        )
        let attributes = field_revisions(
          self.structure.attributes,
          before.attributes,
          after.attributes,
        )
        if !measures.is_empty() || !attributes.is_empty() {
          changed.push({ key: before.key.copy(), measures, attributes, })
        }
      }
    }
  }
  let next_keys : Array[String] = next.index.keys().collect()
  next_keys.sort_by((a, b) => a.lexical_compare(b))
  for key in next_keys {
    if !self.index.contains(key) {
      added.push(detached_observation(next.rows[next.index[key]]))
    }
  }
  {
    structure_profile: self.structure.to_profile_json(),
    added,
    removed,
    changed,
    limits: self.structure.limits,
  }
}

///|
fn field_revision_json(field : FieldRevision) -> Json {
  {
    "component": field.component,
    "before": optional_text_json(field.before),
    "after": optional_text_json(field.after),
  }
}

///|
pub fn DatasetRevision::to_json(
  self : DatasetRevision,
) -> String raise SdmxError {
  canonical_json(
    {
      "profile": "moonsdmx.revision/1",
      "structure": self.structure_profile,
      "added": self.added.map(revision_row_json),
      "removed": self.removed.map(revision_row_json),
      "changed": self.changed.map(change => {
        "key": change.key.map(Json::string),
        "measures": change.measures.map(field_revision_json),
        "attributes": change.attributes.map(field_revision_json),
      }),
    },
    self.limits,
  )
}