///|
fn unit_kind(value : Value?) -> UnitKind {
  match value {
    Some(Enumeration(name)) =>
      match name.to_upper() {
        "LENGTHUNIT" => Length
        "AREAUNIT" => Area
        "VOLUMEUNIT" => Volume
        "MASSUNIT" => Mass
        "TIMEUNIT" => Time
        "PLANEANGLEUNIT" => PlaneAngle
        _ => UnknownUnit
      }
    Some(Typed(name~, value~)) => {
      ignore(name)
      unit_kind(Some(value))
    }
    _ => UnknownUnit
  }
}

///|
fn prefix_scale(value : Value?) -> (String?, Double) {
  match value {
    Some(Enumeration(name)) =>
      match name.to_upper() {
        "EXA" => (Some(name), 1.0e18)
        "PETA" => (Some(name), 1.0e15)
        "TERA" => (Some(name), 1.0e12)
        "GIGA" => (Some(name), 1.0e9)
        "MEGA" => (Some(name), 1.0e6)
        "KILO" => (Some(name), 1.0e3)
        "HECTO" => (Some(name), 1.0e2)
        "DECA" => (Some(name), 1.0e1)
        "DECI" => (Some(name), 1.0e-1)
        "CENTI" => (Some(name), 1.0e-2)
        "MILLI" => (Some(name), 1.0e-3)
        "MICRO" => (Some(name), 1.0e-6)
        "NANO" => (Some(name), 1.0e-9)
        _ => (Some(name), 1.0)
      }
    _ => (None, 1.0)
  }
}

///|
fn si_base_scale(name : String, kind : UnitKind) -> Double {
  match (kind, name.to_upper()) {
    (Length, "METRE") => 1.0
    (Area, "SQUARE_METRE") => 1.0
    (Volume, "CUBIC_METRE") => 1.0
    (Mass, "GRAM") => 1.0e-3
    (Mass, "KILOGRAM") => 1.0
    (Time, "SECOND") => 1.0
    (PlaneAngle, "RADIAN") => 1.0
    (PlaneAngle, "DEGREE") => 0.017453292519943295
    _ => 1.0
  }
}

///|
fn decode_si_unit(entity : Entity) -> UnitScale {
  let kind = unit_kind(entity_value(entity, 1))
  let prefix_value = entity_value(entity, 2)
  let (prefix, prefix_factor) = prefix_scale(prefix_value)
  let name = optional_string_argument(entity, 3).unwrap_or("UNKNOWN")
  { kind, name, prefix, to_si: prefix_factor * si_base_scale(name, kind) }
}

///|
pub fn unit_system(index : ModelIndex) -> UnitSystem {
  let units = index
    .ids_named("IFCSIUNIT")
    .fold(init=Array::new(), (result, id) => {
      match index.entity(id) {
        Some(entity) => {
          result.push(decode_si_unit(entity))
          result
        }
        None => result
      }
    })
  UnitSystem::{ units, }
}

///|
pub fn UnitSystem::find(self : UnitSystem, kind : UnitKind) -> UnitScale? {
  for unit in self.units {
    if unit.kind == kind {
      return Some(unit)
    }
  }
  None
}

///|
pub fn UnitSystem::scale(self : UnitSystem, kind : UnitKind) -> Double {
  match self.find(kind) {
    Some(unit) => unit.to_si
    None => 1.0
  }
}

///|
pub fn UnitSystem::to_si(
  self : UnitSystem,
  kind : UnitKind,
  value : Double,
) -> Double {
  value * self.scale(kind)
}

///|
pub fn UnitSystem::from_si(
  self : UnitSystem,
  kind : UnitKind,
  value : Double,
) -> Double {
  value / self.scale(kind)
}

///|
pub fn UnitSystem::length_to_meters(
  self : UnitSystem,
  value : Double,
) -> Double {
  self.to_si(Length, value)
}

///|
pub fn UnitSystem::meters_to_length(
  self : UnitSystem,
  value : Double,
) -> Double {
  self.from_si(Length, value)
}

///|
pub fn UnitSystem::area_to_square_meters(
  self : UnitSystem,
  value : Double,
) -> Double {
  self.to_si(Area, value)
}

///|
pub fn UnitSystem::volume_to_cubic_meters(
  self : UnitSystem,
  value : Double,
) -> Double {
  self.to_si(Volume, value)
}