///|
pub struct Structure {
  priv reference : StructureRef
  priv dimensions : Array[Component]
  priv measures : Array[Component]
  priv attributes : Array[Component]
  priv positions : Map[String, Int32]
  priv limits : Limits
} derive(@debug.Debug)

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

///|
pub fn Structure::new(
  reference : StructureRef,
  components : Array[Component],
  limits~ : Limits = Limits::standard(),
) -> Structure raise SdmxError {
  if components.length() > limits.max_columns {
    raise Limit(resource="components", maximum=limits.max_columns)
  }
  let ids = components.map(c => c.id)
  unique_names(ids, "components")
  let reserved = [
    "STRUCTURE", "STRUCTURE_ID", "STRUCTURE_NAME", "ACTION", "SERIES_KEY", "OBS_KEY",
  ]
  let dimensions : Array[Component] = []
  let measures : Array[Component] = []
  let attributes : Array[Component] = []
  let positions : Map[String, Int32] = Map([])
  let mut times = 0
  for component in components {
    if reserved.contains(component.id) {
      raise Invalid(path="component.id", reason="reserved exchange header")
    }
    let c = { ..component, role: detached_role(component.role), }
    match c.role {
      Dimension => {
        positions[c.id] = dimensions.length()
        dimensions.push(c)
      }
      TimeDimension => {
        times += 1
        if !(c.representation is Period) {
          raise Invalid(
            path=c.id,
            reason="time dimension must use Period representation",
          )
        }
        positions[c.id] = dimensions.length()
        dimensions.push(c)
      }
      Measure => measures.push(c)
      Attribute(_) => attributes.push(c)
    }
  }
  if dimensions.is_empty() || measures.is_empty() || times > 1 {
    raise Invalid(
      path="structure",
      reason="need dimensions, measures and at most one time dimension",
    )
  }
  for attribute in attributes {
    if attribute.role is Attribute(keys) {
      for key in keys {
        if !positions.contains(key) {
          raise Invalid(
            path=attribute.id,
            reason="attachment references unknown dimension: \{key}",
          )
        }
      }
    }
  }
  { reference, dimensions, measures, attributes, positions, limits, }
}

///|
fn detached_component(c : Component) -> Component {
  { ..c, role: detached_role(c.role), }
}

///|
pub fn Structure::reference(self : Structure) -> StructureRef {
  self.reference
}

///|
pub fn Structure::dimensions(self : Structure) -> Array[Component] {
  self.dimensions.map(detached_component)
}

///|
pub fn Structure::measures(self : Structure) -> Array[Component] {
  self.measures.map(detached_component)
}

///|
pub fn Structure::attributes(self : Structure) -> Array[Component] {
  self.attributes.map(detached_component)
}

///|
pub fn Structure::components(self : Structure) -> Array[Component] {
  let all = self.dimensions()
  for c in self.measures {
    all.push(detached_component(c))
  }
  for c in self.attributes {
    all.push(detached_component(c))
  }
  all
}

///|
pub fn Structure::dimension_index(self : Structure, id : String) -> Int32? {
  self.positions.get(id)
}

///|
pub fn Structure::time_index(self : Structure) -> Int32? {
  for i, c in self.dimensions {
    if c.role is TimeDimension {
      return Some(i)
    }
  }
  None
}

///|
pub fn Structure::component(self : Structure, id : String) -> Component? {
  for c in self.dimensions {
    if c.id == id {
      return Some(detached_component(c))
    }
  }
  for c in self.measures {
    if c.id == id {
      return Some(detached_component(c))
    }
  }
  for c in self.attributes {
    if c.id == id {
      return Some(detached_component(c))
    }
  }
  None
}

///|
fn attachment_key(
  structure : Structure,
  attribute : Component,
  key : Array[String],
) -> String {
  let values : Array[String] = []
  if attribute.role is Attribute(keys) {
    for id in keys {
      values.push(key[structure.positions[id]])
    }
  }
  framed_key(values)
}