#### libremiliacr
#### Copyright(C) 2024 Remilia Scarlet <remilia@posteo.jp>
####
#### This program is free software: you can redistribute it and/or modify
#### it under the terms of the GNU General Public License as published
#### the Free Software Foundation, either version 3 of the License, or
#### (at your option) any later version.
####
#### This program is distributed in the hope that it will be useful,
#### but WITHOUT ANY WARRANTY; without even the implied warranty of
#### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
#### GNU General Public License for more details.
####
#### You should have received a copy of the GNU General Public License
#### along with this program.If not, see<http:####www.gnu.org/licenses/.>
require "big"

####
#### Unit Conversions
####
#### Basic conversions to/from various commonly used units.
####

# The `RemiMath::Conv` module provides routines to convert between commonly used
# units of measurement.
module RemiMath::Conv
  # A unit of measurement.
  enum Unit
    ##
    ## Distance
    ##
    Inches
    Feet
    Picometers
    Nanometers
    Micrometers
    Millimeters
    Centimeters
    Miles
    NauticalMiles
    Kilometers
    Parsecs
    Lightyears

    ##
    ## Speed
    ##
    MilesPerHour
    KilometersPerHour
    Knots

    ##
    ## Mass/Weight
    ##
    Kilograms
    Pounds

    ##
    ## Temperature
    ##
    Celsius
    Fahrenheit
    Kelvin
    Rankine

    ##
    ## Angle/Circle stuff
    ##
    Degrees
    Radians

    ##
    ## Liquids
    ##
    Liters
    UsGallons
    UsPints

    # Similar to `Unit.parse?`, except that this also understands abbreviations
    # and alternate spellings.
    def self.parseUnit?(name : String) : Unit?
      unless ret = self.parse?(name.downcase)
        # Try the abbreviations/alternate spellings
        ret = case name.downcase
              when "in", "inch" then Inches
              when "ft", "foot" then Feet
              when "cm", "centimetres", "centimetre", "centimeter" then Centimeters
              when "mm", "millimetres", "millimetre", "millimeter" then Millimeters
              when "µm", "um", "micrometres", "micron", "microns", "micrometre", "micrometer" then  Micrometers
              when "nm", "nanometres", "nanometre", "nanometer" then Nanometers
              when "pm", "picometres", "picometre", "picometer" then Picometers
              when "mi", "mile" then Miles
              when "nmi", "nautical mile", "nautical miles" then NauticalMiles
              when "km", "kilometres", "kilometre", "kilometer" then Kilometers
              when "pc", "parsec" then Parsecs
              when "ly", "lightyear", "light year", "light years" then Lightyears
              when "mph", "miles per hour" then MilesPerHour
              when "kph", "kilometers per hour", "kilometres per hour" then KilometersPerHour
              when "kt" then Knots
              when "kg", "kilogram" then Kilograms
              when "lb", "pound" then Pounds
              when "c" then Celsius
              when "f", "freedom units" then Fahrenheit
              when "k" then Kelvin
              when "r" then Rankine
              when "deg" then Degrees
              when "rad" then Radians
              when "l", "litres", "litre", "liter" then Liters
              when "gal", "gallons", "gallon" then UsGallons
              when "pt", "pints", "pint" then UsPints
              else nil
              end
      end
      ret
    end

    # Returns the "fancy name" for this `Unit`.  This is a bit different than
    # `#to_s` and `Unit.names` in that it is downcase and may be an alternate
    # name (e.g. `Unit::UsPints` = `"pints"`).
    #
    # The name returned here can be re-parsed using `Unit.parseUnit?`.
    def fancyName : String
      case self
      in Inches, Feet, Picometers, Nanometers, Micrometers, Millimeters, Centimeters, Miles, Kilometers, Parsecs,
         Lightyears,
         Kilograms, Pounds,
         Celsius, Fahrenheit, Kelvin, Rankine,
         Degrees, Radians, Liters
        self.to_s.downcase
      in NauticalMiles then "nauticle miles"
      in MilesPerHour then "miles per hour"
      in KilometersPerHour then "kilometers per hour"
      in Knots then "knots"
      in UsGallons then "gallons"
      in UsPints then "pints"
      end
    end

    # Returns the abbreviation for this `Unit`.
    def abbrev : String
      case self
      in Inches then "in"
      in Feet then "ft"
      in Centimeters then "cm"
      in Millimeters then "mm"
      in Micrometers then "µm"
      in Nanometers then "nm"
      in Picometers then "pm"
      in Miles then "mi"
      in NauticalMiles then "nmi"
      in Kilometers then "km"
      in Parsecs then "pc"
      in Lightyears then "ly"
      in MilesPerHour then "mph"
      in KilometersPerHour then "kph"
      in Knots then "kt"
      in Kilograms then "kg"
      in Pounds then "lb"
      in Celsius then "c"
      in Fahrenheit then "f"
      in Kelvin then "k"
      in Rankine then "r"
      in Degrees then "deg"
      in Radians then "rad"
      in Liters then "l"
      in UsGallons then "gal"
      in UsPints then "pt"
      end
    end

    # Attempts to convert *self* to a new `Unit` value, returned as a
    # `BigDecimal`.  If the conversion cannot happen, this returns `nil`.
    @[AlwaysInline]
    def conv(other : Unit, value) : BigDecimal?
      val : BigDecimal = BigDecimal.new(value)

      case self
      ###
      ### Distance
      ###
      in Inches then convInches(other, val)
      in Feet then convFeet(other, val)
      in Centimeters then convCentimeters(other, val)
      in Millimeters then convMillimeters(other, val)
      in Micrometers then convMicrometers(other, val)
      in Nanometers then convNanometers(other, val)
      in Picometers then convPicometers(other, val)
      in Miles then convMiles(other, val)
      in NauticalMiles then convNauticalMiles(other, val)
      in Kilometers then convKilometers(other, val)
      in Parsecs then convParsecs(other, val)
      in Lightyears then convLightyears(other, val)

      ###
      ### Speed
      ###
      in MilesPerHour then convMilesPerHour(other, val)
      in KilometersPerHour then convKilometersPerHour(other, val)
      in Knots then convKnots(other, val)

      ###
      ### Mass
      ###
      in Kilograms then convKilograms(other, val)
      in Pounds then convPounds(other, val)

      ###
      ### Temperature
      ###
      in Celsius then convCelsius(other, val)
      in Fahrenheit then convFahrenheit(other, val)
      in Kelvin then convKelvin(other, val)
      in Rankine then convRankine(other, val)

      ###
      ### Circle/Angle Stuff
      ###
      in Degrees then convDegrees(other, val)
      in Radians then convRadians(other, val)

      ###
      ### Liquids
      ###
      in Liters then convLiters(other, val)
      in UsGallons then convUsGallons(other, val)
      in UsPints then convUsPints(other, val)
      end
    end

    ############################################################################
    ###
    ### Private Conversion Methods
    ###

    ###
    ### Distance
    ###

    private def convInches(other : Unit, val : BigDecimal) : BigDecimal?
      case other
      when Inches then val
      when Feet then val / 12
      when Centimeters then val * 2.54
      when Millimeters then val * 25.4
      when Micrometers then val * 25400
      when Nanometers then val * 25_400_000
      when Picometers then val * 2_540_0000_000
      when Miles then val / 63360
      when NauticalMiles then val / 72913.385827
      when Kilometers then val / 39370.07874
      when Parsecs then val / BigDecimal.new("1214833693417291800")
      when Lightyears then val / BigDecimal.new("372466929133858300")
      else nil
      end
    end

    private def convFeet(other : Unit, val : BigDecimal) : BigDecimal?
      case other
      when Inches then val * 12
      when Feet then val
      when Centimeters then val * 30.48
      when Millimeters then val * 304.8
      when Micrometers then val * 304800
      when Nanometers then val * 304_800_000
      when Picometers then val * 304_800_000_000i64
      when Miles then val / 5280
      when NauticalMiles then val / 6076.1154856
      when Kilometers then val / 3280.8
      when Parsecs then val / BigDecimal.new("14577600000000000000")
      when Lightyears then val / BigDecimal.new("31038910761154856")
      else nil
      end
    end

    private def convCentimeters(other : Unit, val : BigDecimal) : BigDecimal?
      case other
      when Inches then val / 2.54
      when Feet then val / 30.48
      when Centimeters then val
      when Millimeters then val * 10
      when Micrometers then val * 10000
      when Nanometers then val * 10_000_000
      when Picometers then val * 10_000_000_000
      when Miles then val / 160_930.4
      when NauticalMiles then val / 185_200
      when Kilometers then val / 100_000
      when Parsecs then val * BigDecimal.new("0.00000000000000000032408")
      when Lightyears then val / BigDecimal.new("946066000000000000")
      else nil
      end
    end

    private def convMillimeters(other : Unit, val : BigDecimal) : BigDecimal?
      case other
      when Inches then val * 0.0393700787
      when Feet then val * 0.0032808399
      when Centimeters then val / 10
      when Millimeters then val
      when Micrometers then val * 1000
      when Nanometers then val * 1_000_000
      when Picometers then val * 1_000_000_000
      when Miles then val * BigDecimal.new("0.00000062137129223733")
      when NauticalMiles then val / 1_852_000
      when Kilometers then val * 0.000001
      when Parsecs then val * BigDecimal.new("0.000000000000000000032407792896664")
      when Lightyears then val / BigDecimal.new("9460660000000000000")
      else nil
      end
    end

    private def convMicrometers(other : Unit, val : BigDecimal) : BigDecimal?
      case other
      when Inches then val * 0.0000393701
      when Feet then val * BigDecimal.new("3.2808398950131E-6")
      when Centimeters then val / 10000
      when Millimeters then val / 1000
      when Micrometers then val
      when Nanometers then val * 1000
      when Picometers then val * 1_000_000
      when Miles then val * BigDecimal.new("6.2137119223733E-10")
      when NauticalMiles then val / 1_852_000_000i64
      when Kilometers then val * BigDecimal.new("1.0E-9")
      when Parsecs then val * BigDecimal.new("3.2407792896664E-23")
      when Lightyears then val / BigDecimal.new("9.46066E+21")
      else nil
      end
    end

    private def convNanometers(other : Unit, val : BigDecimal) : BigDecimal?
      case other
      when Inches then val * BigDecimal.new("3.9370078740157E-8")
      when Feet then val * BigDecimal.new("3.2808398950131E-9")
      when Centimeters then val / 10_000_000
      when Millimeters then val / 1_000_000
      when Micrometers then val / 1000
      when Nanometers then val
      when Picometers then val * 1000
      when Miles then val * BigDecimal.new("6.2137119223733E-13")
      when NauticalMiles then val / 1_852_000_000_000i64
      when Kilometers then val * BigDecimal.new("1.0E-12")
      when Parsecs then val * BigDecimal.new("3.2407792896664E-26")
      when Lightyears then val / BigDecimal.new("9.460659999E+24")
      else nil
      end
    end

    private def convPicometers(other : Unit, val : BigDecimal) : BigDecimal?
      case other
      when Inches then val * BigDecimal.new("3.9370078740157E-11")
      when Feet then val * BigDecimal.new("3.2808398950131E-12")
      when Centimeters then val / 10_000_000_000
      when Millimeters then val / 1_000_000_000
      when Micrometers then val / 1_000_000
      when Nanometers then val / 1000
      when Picometers then val
      when Miles then val * BigDecimal.new("6.2137119223733E-16")
      when NauticalMiles then val / BigDecimal.new("1852000000000012")
      when Kilometers then val * BigDecimal.new("1.0E-15")
      when Parsecs then val * BigDecimal.new("3.2407792896664E-29")
      when Lightyears then val * BigDecimal.new("1.0570008340247E-28")
      else nil
      end
    end

    private def convMiles(other : Unit, val : BigDecimal) : BigDecimal?
      case other
      when Inches then val * 63360
      when Feet then val * 5280
      when Centimeters then val * 160_930.4
      when Millimeters then val * 1_609_344
      when Micrometers then val * 1_609_344_000
      when Nanometers then val * 1_609_344_000_000i64
      when Picometers then val * BigDecimal.new("1.609344E+15")
      when Miles then val
      when NauticalMiles then val / 1.150779448
      when Kilometers then val * 1.690344
      when Parsecs then val * BigDecimal.new("0.000000000000052155")
      when Lightyears then val / BigDecimal.new("5878559666946")
      else nil
      end
    end

    private def convNauticalMiles(other : Unit, val : BigDecimal) : BigDecimal?
      case other
      when Inches then val * 72913.385827
      when Feet then val * 6076.1154856
      when Centimeters then val * 185_200
      when Millimeters then val * 1_852_000
      when Micrometers then val * 1_852_000_000
      when Nanometers then val * 1_852_000_000_000i64
      when Picometers then val * BigDecimal.new("1852000000000012")
      when Miles then val * 1.150779448
      when NauticalMiles then val
      when Kilometers then val * 1.852
      when Parsecs then val / BigDecimal.new("16661326032829")
      when Lightyears then val / BigDecimal.new("5108385784330")
      else nil
      end
    end

    private def convKilometers(other : Unit, val : BigDecimal) : BigDecimal?
      case other
      when Inches then val * 39370.0787
      when Feet then val * 3280.8
      when Centimeters then val * 100_000
      when Millimeters then val * 1_000_000
      when Micrometers then val * 1_000_000_000
      when Nanometers then val * 1_000_000_000_000i64
      when Picometers then val * BigDecimal.new("1.0E+15")
      when Miles then val / 1.609344
      when NauticalMiles then val / 1.852
      when Kilometers then val
      when Parsecs then val * BigDecimal.new("0.000000000000032408")
      when Lightyears then val / BigDecimal.new("9460660000000")
      else nil
      end
    end

    private def convParsecs(other : Unit, val : BigDecimal) : BigDecimal?
      case other
      when Inches then val * BigDecimal.new("1214800000000000000")
      when Feet then val * BigDecimal.new("1214800000000000000")
      when Centimeters then val / BigDecimal.new("0.00000000000000000032408")
      when Millimeters then val * BigDecimal.new("3.08567758128E+19")
      when Micrometers then val * BigDecimal.new("3.08567758128E+22")
      when Nanometers then val * BigDecimal.new("3.08567758128E+25")
      when Picometers then val * BigDecimal.new("3.08567758128E+28")
      when Miles then val * BigDecimal.new("19174000000000")
      when NauticalMiles then val * BigDecimal.new("16661326032829")
      when Kilometers then val / BigDecimal.new("0.000000000000032408")
      when Parsecs then val
      when Lightyears then val * 3.2615637769
      else nil
      end
    end

    private def convLightyears(other : Unit, val : BigDecimal) : BigDecimal?
      case other
      when Inches then val * BigDecimal.new("3.7246970364488E+17")
      when Feet then val * BigDecimal.new("3.1039141970407E+16")
      when Centimeters then val * BigDecimal.new("9.46073047258E+17")
      when Millimeters then val * BigDecimal.new("9.46073047258E+18")
      when Micrometers then val * BigDecimal.new("9.46073047258E+21")
      when Nanometers then val * BigDecimal.new("9.46073047258E+24")
      when Picometers then val * BigDecimal.new("9.46073047258E+27")
      when Miles then val * BigDecimal.new("5878625373183.1")
      when NauticalMiles then val * BigDecimal.new("5108385784330")
      when Kilometers then val * BigDecimal.new("9460730472580")
      when Parsecs then val * 0.3066013938
      when Lightyears then val
      else nil
      end
    end

    ###
    ### Speed
    ###

    private def convMilesPerHour(other : Unit, val : BigDecimal) : BigDecimal?
      case other
      when MilesPerHour then val
      when KilometersPerHour then val / 0.6213711922
      when Knots then val / 1.150779448
      else nil
      end
    end

    private def convKilometersPerHour(other : Unit, val : BigDecimal) : BigDecimal?
      case other
      when MilesPerHour then val / 1.609344
      when KilometersPerHour then val
      when Knots then val / 1.852
      else nil
      end
    end

    private def convKnots(other : Unit, val : BigDecimal) : BigDecimal?
      case other
      when MilesPerHour then val * 1.150779448
      when KilometersPerHour then val * 1.852
      when Knots then val
      else nil
      end
    end

    ###
    ### Mass/Weight
    ###

    private def convKilograms(other : Unit, val : BigDecimal) : BigDecimal?
      case other
      when Kilograms then val
      when Pounds then val * 2.2046244202
      else nil
      end
    end

    private def convPounds(other : Unit, val : BigDecimal) : BigDecimal?
      case other
      when Kilograms then val * BigDecimal.new("0.453592")
      when Pounds then val
      else nil
      end
    end

    ###
    ### Temperature
    ###

    private def convCelsius(other : Unit, val : BigDecimal) : BigDecimal?
      case other
      when Celsius then val
      when Fahrenheit then (val * (9 / 5)) + 32
      when Kelvin then val + 273.15
      when Rankine then val * (9 / 5) + 491.67
      else nil
      end
    end

    private def convFahrenheit(other : Unit, val : BigDecimal) : BigDecimal?
      case other
      when Celsius then (val - 32) * (5 / 9)
      when Fahrenheit then val
      when Kelvin then ((val - 32) * (5 / 9)) + 273.15
      when Rankine then val + 459.67
      else nil
      end
    end

    private def convKelvin(other : Unit, val : BigDecimal) : BigDecimal?
      case other
      when Celsius then val - 273.15
      when Fahrenheit then ((val - 273.15) * (9 / 5)) + 32
      when Kelvin then val
      when Rankine then ((val - 273.15) * (9 / 5)) + 491.67
      else nil
      end
    end

    private def convRankine(other : Unit, val : BigDecimal) : BigDecimal?
      case other
      when Celsius then (val - 491.67) * (5 / 9)
      when Fahrenheit then val - 459.67
      when Kelvin then val * (5 / 9)
      when Rankine then val
      else nil
      end
    end

    ###
    ### Circle/Angle Stuff
    ###

    private def convDegrees(other : Unit, val : BigDecimal) : BigDecimal?
      case other
      when Degrees then val
      when Radians then val * (Math::PI / 180)
      else nil
      end
    end

    private def convRadians(other : Unit, val : BigDecimal) : BigDecimal?
      case other
      when Degrees then val * (180 / Math::PI)
      when Radians then val
      else nil
      end
    end

    ###
    ### Liquids
    ###

    private def convLiters(other : Unit, val : BigDecimal) : BigDecimal?
      case other
      when Liters then val
      when UsGallons then val * 0.26417
      when UsPints then val * 2.1134
      else nil
      end
    end

    private def convUsGallons(other : Unit, val : BigDecimal) : BigDecimal?
      case other
      when Liters then val / 0.26417
      when UsGallons then val
      when UsPints then val * 8
      else nil
      end
    end

    private def convUsPints(other : Unit, val : BigDecimal) : BigDecimal?
      case other
      when Liters then val / 2.1134
      when UsGallons then val / 8
      when UsPints then val
      else nil
      end
    end
  end
end
