///|
pub enum UnitDimension {
  Mass
  Amount
  Volume
  Length
  Pressure
  Energy
  Power
  Temperature
  Time
  Dimensionless
} derive(Debug, Eq)

///|
pub enum MeasureUnit {
  Kilogram
  Gram
  Tonne
  KgPerHour
  Mol
  Kmol
  Liter
  CubicMeter
  CubicMeterPerHour
  Meter
  Pascal
  KiloPascal
  Bar
  Joule
  KiloJoule
  KiloWatt
  Celsius
  Kelvin
  Second
  Minute
  Hour
  Percent
  Ratio
} derive(Debug, Eq)

///|
pub enum QuantityError {
  IncompatibleDimensions(UnitDimension, UnitDimension)
  BelowAbsoluteZero
  InvalidValue
} derive(Debug, Eq)

///|
pub struct Quantity {
  value : Float
  unit : MeasureUnit
} derive(Debug, Eq)

///|
pub fn quantity(value : Float, unit : MeasureUnit) -> Quantity {
  { value, unit }
}

///|
pub fn MeasureUnit::dimension(self : MeasureUnit) -> UnitDimension {
  match self {
    Kilogram | Gram | Tonne | KgPerHour => Mass
    Mol | Kmol => Amount
    Liter | CubicMeter | CubicMeterPerHour => Volume
    Meter => Length
    Pascal | KiloPascal | Bar => Pressure
    Joule | KiloJoule => Energy
    KiloWatt => Power
    Celsius | Kelvin => Temperature
    Second | Minute | Hour => Time
    Percent | Ratio => Dimensionless
  }
}

///|
fn MeasureUnit::scale_to_base(self : MeasureUnit) -> Float {
  match self {
    Kilogram => 1.0
    Gram => 0.001
    Tonne => 1000.0
    KgPerHour => 1.0
    Mol => 1.0
    Kmol => 1000.0
    Liter => 0.001
    CubicMeter | CubicMeterPerHour => 1.0
    Meter => 1.0
    Pascal => 1.0
    KiloPascal => 1000.0
    Bar => 100000.0
    Joule => 1.0
    KiloJoule => 1000.0
    KiloWatt => 1.0
    Celsius | Kelvin => 1.0
    Second => 1.0
    Minute => 60.0
    Hour => 3600.0
    Percent => 0.01
    Ratio => 1.0
  }
}

///|
pub fn MeasureUnit::symbol(self : MeasureUnit) -> String {
  match self {
    Kilogram => "kg"
    Gram => "g"
    Tonne => "t"
    KgPerHour => "kg/h"
    Mol => "mol"
    Kmol => "kmol"
    Liter => "L"
    CubicMeter => "m3"
    CubicMeterPerHour => "m3/h"
    Meter => "m"
    Pascal => "Pa"
    KiloPascal => "kPa"
    Bar => "bar"
    Joule => "J"
    KiloJoule => "kJ"
    KiloWatt => "kW"
    Celsius => "C"
    Kelvin => "K"
    Second => "s"
    Minute => "min"
    Hour => "h"
    Percent => "%"
    Ratio => "ratio"
  }
}

///|
pub fn Quantity::validate(
  self : Quantity,
) -> Result[MeasureUnit, QuantityError] {
  if self.unit is Celsius && self.value < -273.15 {
    Err(BelowAbsoluteZero)
  } else if self.unit is Kelvin && self.value < 0.0 {
    Err(BelowAbsoluteZero)
  } else if self.value != self.value {
    Err(InvalidValue)
  } else {
    Ok(self.unit)
  }
}

///|
pub fn Quantity::convert_to(
  self : Quantity,
  target : MeasureUnit,
) -> Result[Float, QuantityError] {
  if self.unit.dimension() != target.dimension() {
    return Err(
      IncompatibleDimensions(self.unit.dimension(), target.dimension()),
    )
  }
  match self.validate() {
    Err(error) => Err(error)
    Ok(_) =>
      match self.unit.dimension() {
        Temperature =>
          if self.unit is Celsius && target is Kelvin {
            Ok(self.value + 273.15)
          } else if self.unit is Kelvin && target is Celsius {
            Ok(self.value - 273.15)
          } else {
            Ok(self.value)
          }
        _ => Ok(self.value * self.unit.scale_to_base() / target.scale_to_base())
      }
  }
}

///|
pub fn Quantity::in_unit(
  self : Quantity,
  target : MeasureUnit,
) -> Result[Quantity, QuantityError] {
  match self.convert_to(target) {
    Ok(value) => Ok({ value, unit: target })
    Err(error) => Err(error)
  }
}

///|
pub fn Quantity::add(
  self : Quantity,
  other : Quantity,
) -> Result[Quantity, QuantityError] {
  match other.convert_to(self.unit) {
    Ok(value) => Ok({ value: self.value + value, unit: self.unit })
    Err(error) => Err(error)
  }
}

///|
pub fn Quantity::subtract(
  self : Quantity,
  other : Quantity,
) -> Result[Quantity, QuantityError] {
  match other.convert_to(self.unit) {
    Ok(value) => Ok({ value: self.value - value, unit: self.unit })
    Err(error) => Err(error)
  }
}

///|
pub fn Quantity::scale(self : Quantity, factor : Float) -> Quantity {
  { value: self.value * factor, unit: self.unit }
}

///|
pub fn Quantity::format(self : Quantity, decimals : Int) -> String {
  "\{self.value} \{self.unit.symbol()} (\{decimals} dp)"
}

///|
pub fn parse_unit(text : String) -> MeasureUnit? {
  match text.trim() {
    "kg" => Some(Kilogram)
    "g" => Some(Gram)
    "t" => Some(Tonne)
    "kg/h" => Some(KgPerHour)
    "mol" => Some(Mol)
    "kmol" => Some(Kmol)
    "L" => Some(Liter)
    "m3" => Some(CubicMeter)
    "m3/h" => Some(CubicMeterPerHour)
    "m" => Some(Meter)
    "Pa" => Some(Pascal)
    "kPa" => Some(KiloPascal)
    "bar" => Some(Bar)
    "J" => Some(Joule)
    "kJ" => Some(KiloJoule)
    "kW" => Some(KiloWatt)
    "C" => Some(Celsius)
    "K" => Some(Kelvin)
    "s" => Some(Second)
    "min" => Some(Minute)
    "h" => Some(Hour)
    "%" => Some(Percent)
    "ratio" => Some(Ratio)
    _ => None
  }
}

///|
pub fn bar() -> MeasureUnit {
  Bar
}

///|
pub fn pascal() -> MeasureUnit {
  Pascal
}

///|
pub fn kelvin() -> MeasureUnit {
  Kelvin
}

///|
pub fn celsius() -> MeasureUnit {
  Celsius
}

///|
pub fn kg_per_hour() -> MeasureUnit {
  KgPerHour
}

///|
pub fn compatible(left : MeasureUnit, right : MeasureUnit) -> Bool {
  left.dimension() == right.dimension()
}

///|
pub fn base_unit(dimension : UnitDimension) -> MeasureUnit {
  match dimension {
    Mass => Kilogram
    Amount => Mol
    Volume => CubicMeter
    Length => Meter
    Pressure => Pascal
    Energy => Joule
    Power => KiloWatt
    Temperature => Kelvin
    Time => Second
    Dimensionless => Ratio
  }
}