!*******************************************************************************
!> @brief Unit conversion module.
!*******************************************************************************
!> Converts length, energy and mass units. Units can either by accessed by ID
!> number (UNITS_*) or through a type Units. This module should be initialized
!> before use (Initialize_UnitConversion_Module).
!>
!> @author Mark Wijzenbroek
!> @date 2012
!*******************************************************************************
MODULE UnitConversion

!> A set of units.
  TYPE Units
    INTEGER :: LengthUnit, EnergyUnit, MassUnit, TimeUnit
  END TYPE

! Length units
  INTEGER, PARAMETER :: UNITS_BOHR     = 0 !< Index for bohr
  INTEGER, PARAMETER :: UNITS_ANGSTROM = 1 !< Index for Angstrom

  REAL, PARAMETER :: CONV_ANGSTROM = 0.529177249 !< Conversion factor from bohr to Angstrom

! Energy units
  INTEGER, PARAMETER :: UNITS_HARTREE = 2 !< Index for Hartree
  INTEGER, PARAMETER :: UNITS_EV      = 3 !< Index for eV
  INTEGER, PARAMETER :: UNITS_KCALMOL = 4 !< Index for kcal/mol
  INTEGER, PARAMETER :: UNITS_KJMOL   = 5 !< Index for kJ/mol
  INTEGER, PARAMETER :: UNITS_JOULE   = 6 !< Index for Joule

  REAL, PARAMETER :: CONV_EV = 27.2113961           !< Conversion factor from Hartree to eV
  REAL, PARAMETER :: CONV_KCALMOL = 627.50956       !< Conversion factor from Hartree to kcal/mol
  REAL, PARAMETER :: CONV_KJMOL = 627.50956 * 4.184 !< Conversion factor from Hartree to kJ/mol
  REAL, PARAMETER :: CONV_JOULE = 4.35974434e-18    !< Conversion factor from Hartree to Joule

! Mass units
  INTEGER, PARAMETER :: UNITS_MELEC = 7 !< Index for electron mass
  INTEGER, PARAMETER :: UNITS_MPROT = 8 !< Index for proton mass
  INTEGER, PARAMETER :: UNITS_AMU = 9   !< Index for amu
  INTEGER, PARAMETER :: UNITS_KG = 10   !< Index for kg

  REAL, PARAMETER :: CONV_MPROT = 1/1836.153  !< Conversion factor from electron mass to proton mass
  REAL, PARAMETER :: CONV_AMU = 1/1822.88839  !< Conversion factor from electron mass to amu
  REAL, PARAMETER :: CONV_KG = 9.10938291e-31 !< Conversion factor from electron mass to kg

! Time units
  INTEGER, PARAMETER :: UNITS_AUT = 11 !< Index for atomic units of time
  INTEGER, PARAMETER :: UNITS_PS = 12  !< Index for picoseconds
  INTEGER, PARAMETER :: UNITS_FS = 13  !< Index for femtoseconds

  REAL, PARAMETER :: CONV_PS = 2.418884e-5 !< Conversion factor from atomic units of time to picoseconds
  REAL, PARAMETER :: CONV_FS = 2.418884e-2 !< Conversion factor from atomic units of time to femtoseconds

!> A wrapper for converting length units.
  INTERFACE Convert_Length_Units
    MODULE PROCEDURE Convert_Length_Units_Type, &
                     Convert_Length_Units_Int
  END INTERFACE

!> A wrapper for converting energy units.
  INTERFACE Convert_Energy_Units
    MODULE PROCEDURE Convert_Energy_Units_Type, &
                     Convert_Energy_Units_Int
  END INTERFACE

!> A wrapper for converting mass units.
  INTERFACE Convert_Mass_Units
    MODULE PROCEDURE Convert_Mass_Units_Type, &
                     Convert_Mass_Units_Int
  END INTERFACE

!> A wrapper for converting time units.
  INTERFACE Convert_Time_Units
    MODULE PROCEDURE Convert_Time_Units_Type, &
                     Convert_Time_Units_Int
  END INTERFACE

!> Units internally used in the program (set to atomic units).
  TYPE ( Units ) :: InternalUnits

  INTEGER :: UnitConversion_Module_SetUp = 0

CONTAINS

!*******************************************************************************
! Convert_Length_Units_Type
!*******************************************************************************
!> Convert length units with a Units type.
!>
!> @param Num The number that has to be converted.
!> @param InUnits A Units with the input units.
!> @param OutUnits A Units with the output units.
!> @returns The number in the new units.
!*******************************************************************************
  FUNCTION Convert_Length_Units_Type( Num, InUnits, OutUnits ) RESULT ( Val )
    IMPLICIT NONE

    TYPE (Units)     :: InUnits, OutUnits
    REAL :: Num, Val

    Val = Convert_Length_Units_Int( Num, InUnits%LengthUnit, OutUnits%LengthUnit )
  END FUNCTION

!*******************************************************************************
! Convert_Length_Units_Int
!*******************************************************************************
!> Convert length units by index.
!>
!> @param Num The number that has to be converted.
!> @param InUnits The index of the input units.
!> @param OutUnits The index of the output units.
!> @returns The number in the new units.
!*******************************************************************************
  FUNCTION Convert_Length_Units_Int( Num, InUnits, OutUnits ) RESULT ( Val )
    IMPLICIT NONE

    INTEGER          :: InUnits, OutUnits
    REAL :: Num, Val
    REAL :: Conversion

    IF ( InUnits .EQ. OutUnits ) THEN
      Val = Num
      RETURN
    END IF

    SELECT CASE ( InUnits )
      CASE ( UNITS_BOHR )
        Conversion = 1.0
      CASE ( UNITS_ANGSTROM )
        Conversion = 1.0 / CONV_ANGSTROM
      CASE DEFAULT
        PRINT *, "Unknown input units"
        STOP
    END SELECT

    SELECT CASE ( OutUnits )
      CASE ( UNITS_BOHR )
      CASE ( UNITS_ANGSTROM )
        Conversion = Conversion * CONV_ANGSTROM
      CASE DEFAULT
        PRINT *, "Unknown output units"
        STOP
    END SELECT

    Val = Num * Conversion
  END FUNCTION

!*******************************************************************************
! Convert_Energy_Units_Type
!*******************************************************************************
!> Convert energy units with a Units type.
!>
!> @param Num The number that has to be converted.
!> @param InUnits A Units with the input units.
!> @param OutUnits A Units with the output units.
!> @returns The number in the new units.
!*******************************************************************************
  FUNCTION Convert_Energy_Units_Type( Num, InUnits, OutUnits ) RESULT ( Val )
    IMPLICIT NONE

    TYPE (Units)     :: InUnits, OutUnits
    REAL :: Num, Val

    Val = Convert_Energy_Units_Int( Num, InUnits%EnergyUnit, OutUnits%EnergyUnit )
  END FUNCTION

!*******************************************************************************
! Convert_Energy_Units_Int
!*******************************************************************************
!> Convert energy units by index.
!>
!> @param Num The number that has to be converted.
!> @param InUnits The index of the input units.
!> @param OutUnits The index of the output units.
!> @returns The number in the new units.
!*******************************************************************************
  FUNCTION Convert_Energy_Units_Int( Num, InUnits, OutUnits ) RESULT ( Val )
    IMPLICIT NONE

    INTEGER          :: InUnits, OutUnits
    REAL :: Num, Val
    REAL :: Conversion

    IF ( InUnits .EQ. OutUnits ) THEN
      Val = Num
      RETURN
    END IF

    SELECT CASE ( InUnits )
      CASE ( UNITS_HARTREE )
        Conversion = 1.0
      CASE ( UNITS_EV )
        Conversion = 1.0 / CONV_EV
      CASE ( UNITS_KCALMOL )
        Conversion = 1.0 / CONV_KCALMOL
      CASE ( UNITS_KJMOL )
        Conversion = 1.0 / CONV_KJMOL
      CASE ( UNITS_JOULE )
        Conversion = 1.0 / CONV_JOULE
      CASE DEFAULT
        PRINT *, "Unknown input units"
        STOP
    END SELECT

    SELECT CASE ( OutUnits )
      CASE ( UNITS_HARTREE )
      CASE ( UNITS_EV )
        Conversion = Conversion * CONV_EV
      CASE ( UNITS_KCALMOL )
        Conversion = Conversion * CONV_KCALMOL
      CASE ( UNITS_KJMOL )
        Conversion = Conversion * CONV_KJMOL
      CASE ( UNITS_JOULE )
        Conversion = Conversion * CONV_JOULE
      CASE DEFAULT
        PRINT *, "Unknown output units"
        STOP
    END SELECT

    Val = Num * Conversion
  END FUNCTION

!*******************************************************************************
! Convert_Time_Units_Type
!*******************************************************************************
!> Convert time units with a Units type.
!>
!> @param Num The number that has to be converted.
!> @param InUnits A Units with the input units.
!> @param OutUnits A Units with the output units.
!> @returns The number in the new units.
!*******************************************************************************
  FUNCTION Convert_Time_Units_Type( Num, InUnits, OutUnits ) RESULT ( Val )
    IMPLICIT NONE

    TYPE (Units)     :: InUnits, OutUnits
    REAL :: Num, Val

    Val = Convert_Time_Units_Int( Num, InUnits%TimeUnit, OutUnits%TimeUnit )
  END FUNCTION

!*******************************************************************************
! Convert_Time_Units_Int
!*******************************************************************************
!> Convert time units by index.
!>
!> @param Num The number that has to be converted.
!> @param InUnits The index of the input units.
!> @param OutUnits The index of the output units.
!> @returns The number in the new units.
!*******************************************************************************
  FUNCTION Convert_Time_Units_Int( Num, InUnits, OutUnits ) RESULT ( Val )
    IMPLICIT NONE

    INTEGER          :: InUnits, OutUnits
    REAL :: Num, Val
    REAL :: Conversion

    IF ( InUnits .EQ. OutUnits ) THEN
      Val = Num
      RETURN
    END IF

    SELECT CASE ( InUnits )
      CASE ( UNITS_AUT )
        Conversion = 1.0
      CASE ( UNITS_PS )
        Conversion = 1.0 / CONV_PS
      CASE ( UNITS_FS )
        Conversion = 1.0 / CONV_FS
      CASE DEFAULT
        PRINT *, "Unknown input units"
        STOP
    END SELECT

    SELECT CASE ( OutUnits )
      CASE ( UNITS_AUT )
      CASE ( UNITS_PS )
        Conversion = Conversion * CONV_PS
      CASE ( UNITS_FS )
        Conversion = Conversion * CONV_FS
      CASE DEFAULT
        PRINT *, "Unknown output units"
        STOP
    END SELECT

    Val = Num * Conversion
  END FUNCTION

!*******************************************************************************
! Convert_Mass_Units_Type
!*******************************************************************************
!> Convert mass units with a Units type.
!>
!> @param Num The number that has to be converted.
!> @param InUnits A Units with the input units.
!> @param OutUnits A Units with the output units.
!> @returns The number in the new units.
!*******************************************************************************
  FUNCTION Convert_Mass_Units_Type( Num, InUnits, OutUnits ) RESULT ( Val )
    IMPLICIT NONE

    TYPE (Units)     :: InUnits, OutUnits
    REAL :: Num, Val

    Val = Convert_Mass_Units_Int( Num, InUnits%MassUnit, OutUnits%MassUnit )
  END FUNCTION

!*******************************************************************************
! Convert_Mass_Units_Int
!*******************************************************************************
!> Convert mass units by index.
!>
!> @param Num The number that has to be converted.
!> @param InUnits The index of the input units.
!> @param OutUnits The index of the output units.
!> @returns The number in the new units.
!*******************************************************************************
  FUNCTION Convert_Mass_Units_Int( Num, InUnits, OutUnits ) RESULT ( Val )
    IMPLICIT NONE

    INTEGER          :: InUnits, OutUnits
    REAL :: Num, Val
    REAL :: Conversion

    IF ( InUnits .EQ. OutUnits ) THEN
      Val = Num
      RETURN
    END IF

    SELECT CASE ( InUnits )
      CASE ( UNITS_MELEC )
        Conversion = 1.0
      CASE ( UNITS_MPROT )
        Conversion = 1.0 / CONV_MPROT
      CASE ( UNITS_AMU )
        Conversion = 1.0 / CONV_AMU
      CASE ( UNITS_KG )
        Conversion = 1.0 / CONV_KG
      CASE DEFAULT
        PRINT *, "Unknown input units"
        STOP
    END SELECT

    SELECT CASE ( OutUnits )
      CASE ( UNITS_MELEC )
      CASE ( UNITS_MPROT )
        Conversion = Conversion * CONV_MPROT
      CASE ( UNITS_AMU )
        Conversion = Conversion * CONV_AMU
      CASE ( UNITS_KG )
        Conversion = Conversion * CONV_KG
      CASE DEFAULT
        PRINT *, "Unknown output units"
        STOP
    END SELECT

    Val = Num * Conversion
  END FUNCTION

!*******************************************************************************
! Convert_Force_Units
!*******************************************************************************
!> Convert force units with a Units type.
!>
!> @param Num The number that has to be converted.
!> @param InUnits A Units with the input units.
!> @param OutUnits A Units with the output units.
!> @returns The number in the new units.
!*******************************************************************************
  FUNCTION Convert_Force_Units( Num, InUnits, OutUnits ) RESULT ( Val )
    IMPLICIT NONE

    TYPE (Units)     :: InUnits, OutUnits
    REAL :: Num, Val

    Val = Num * Convert_Energy_Units( 1.0, InUnits, OutUnits ) / Convert_Length_Units( 1.0, InUnits, OutUnits )
  END FUNCTION

!*******************************************************************************
! Initialize_UnitConversion_Module
!*******************************************************************************
!> Initialize the UnitConversion module. Needs to be run before use!
!*******************************************************************************
  SUBROUTINE Initialize_UnitConversion_Module()
    IMPLICIT NONE

    IF ( UnitConversion_Module_SetUp .LT. 1 ) THEN
      UnitConversion_Module_SetUp = 1
      InternalUnits%LengthUnit = UNITS_BOHR
      InternalUnits%EnergyUnit = UNITS_HARTREE
      InternalUnits%MassUnit   = UNITS_MELEC
      InternalUnits%TimeUnit   = UNITS_AUT
    END IF
  END SUBROUTINE

END MODULE

