! See copyright notice in the COPYRIGHT file.
! ****************************************************************************** !
!> The module contains the time definition of treelm.
!!
!! It is useful to describe the progress in time of an explicit solver with
!! respect to different measurements.
!! Currently accounted are:
!!
!! - iterations
!! - simulation time
!! - running time (clock)
!!
!! For the handling of time evolution in an explicit solver, a time definition
!! is needed to keep track of the current time.
!! This should be initialized with [[tem_time_reset]], and then advanced in each
!! iteration by tem_time_advance.
!! Time definitions in relation to this main time evolution object can then
!! be defined in the Lua configuration by [[tem_time_load]].
!! For an explicit time stepping solver this would at least require one
!! definition of time, when to stop the simulation.
!! A comparison of such points in time to the current time can be done by
!! tem_time_ge_trigger.
module tem_time_module
  use mpi

  use env_module, only: rk, labelLen

  use aotus_module, only: flu_State, &
    &                     aot_get_val
  use aot_out_module, only: aot_out_type,       &
    &                       aot_out_val,        &
    &                       aot_out_open_table, &
    &                       aot_out_close_table
  use aot_table_module, only: aot_table_open, &
    &                         aot_table_close

  implicit none

  private

  public :: tem_time_type

  public :: tem_time_load
  public :: tem_time_dump
  public :: tem_time_out
  public :: tem_time_ge_each
  public :: tem_time_ge_trigger
  public :: tem_time_gt_trigger
  public :: tem_time_reset
  public :: tem_time_set_clock
  public :: tem_time_advance
  public :: tem_time_never
  public :: tem_time_isDefined
  public :: tem_time_last_interval
  public :: tem_time_default_zero

  public :: operator(+)
  public :: operator(-)
  public :: max

  public :: tem_time_needs_reduce
  public :: tem_time_sim_stamp
  public :: tem_time_iter_stamp

  !> Number of available different time definitions.
  integer, parameter, public :: tem_time_n_ids = 3


  !> Index for the simulation time.
  integer, parameter, public :: tem_time_sim_id   = 1
  !> Index for the number of iterations.
  integer, parameter, public :: tem_time_iter_id  = 2
  !> Index for the clock time.
  integer, parameter, public :: tem_time_clock_id = 3


  !> Description of time
  !!
  !! This type provides the description of a point in
  !! time in different terms.
  !! Currently supported are:
  !! - number of iterations
  !! - simulation time
  !! - running time (clock)
  !!
  !! It can be used to describe certain points in time
  !! as well as periods of time.
  type tem_time_type
    !> Time in iterations.
    integer :: iter

    !> Time in terms of simulated time.
    real(kind=rk) :: sim

    !> Time passed in seconds of running time.
    real(kind=rk) :: clock

    !> Wtime of the last reset.
    real(kind=rk) :: clock_start
  end type tem_time_type


  interface operator(+)
    module procedure tem_time_add_time
  end interface

  interface operator(-)
    module procedure tem_time_subtract_time
  end interface

  interface max
    module procedure tem_time_max
  end interface


contains


  ! **************************************************************************** !
  !> Reading a time description from a Lua script given by conf.
  !!
  !! The time description has to be provided in the table given by thandle.
  !! The time can be given in terms of:
  !! - iter: number of iterations
  !! - sim: simulated time
  !! - clock: running time used to compute the simulation
  !! Thus, the configuration looks like:
  !! \verbatim
  !! time = { iter = 123, sim = 1.23, clock = 12.3 }
  !! \end verbatim
  !! Omitted time defintions are set to the maximal representable number,
  !! indicating something like never.
  !! If the time is not provided as a table, it is interpreted as a setting of
  !! the sim time, the other entries are set to never.
  !!
  !! The optional argument clock_start is useful to overwrite the current time
  !! by the settings in a loaded restart header.
  !! With clock_start the old reference wtime from tem_time_reset can be
  !! maintained, while iterations and simulation time can be inherited from
  !! the configuration.
  subroutine tem_time_load(me, conf, key, parent, clock_start)
    ! --------------------------------------------------------------------------!
    !> Time to be read from the Lua script
    type(tem_time_type), intent(out) :: me

    !> Handle to the Lua script.
    type(flu_state), intent(inout) :: conf

    !> Name of the table containing the time definition. Default: 'time'.
    character(len=*), intent(in), optional :: key

    !> Handle to the parent table.
    integer, intent(in), optional :: parent

    !> Reference MPI_Wtime mark to use for computation of wallclock
    !!
    !! This is basically only useful to update the time object for a read in
    !! restart without distorting the clock measurement.
    real(kind=rk), intent(in), optional :: clock_start
    ! --------------------------------------------------------------------------!
    integer :: iErr
    character(len=labelLen) :: loc_key
    integer :: thandle
    ! --------------------------------------------------------------------------!

    if ( present(key) ) then
      loc_key = trim(key)
    else
      loc_key = 'time'
    end if

    call aot_table_open(L       = conf,    &
      &                 parent  = parent,  &
      &                 thandle = thandle, &
      &                 key     = loc_key  )

    if (thandle /= 0) then
      ! The time is given as a table load its components accordingly.
      call aot_get_val(L       = conf,         &
        &              thandle = thandle,      &
        &              val     = me%sim,       &
        &              key     = 'sim',        &
        &              default = huge(me%sim), &
        &              ErrCode = iErr          )

      call aot_get_val(L       = conf,          &
        &              thandle = thandle,       &
        &              val     = me%iter,       &
        &              key     = 'iter',        &
        &              default = huge(me%iter), &
        &              ErrCode = iErr           )

      call aot_get_val(L       = conf,           &
        &              thandle = thandle,        &
        &              val     = me%clock,       &
        &              key     = 'clock',        &
        &              default = huge(me%clock), &
        &              ErrCode = iErr            )
    else
      ! The time is not given as a table, try to interpret it as a setting for
      ! the simtime.
      call aot_get_val(L       = conf,         &
        &              thandle = parent,       &
        &              key     = loc_key,      &
        &              val     = me%sim,       &
        &              default = huge(me%sim), &
        &              ErrCode = iErr          )
      me%iter  = huge(me%iter)
      me%clock = huge(me%clock)
    end if

    call aot_table_close(conf, thandle)

    !>\todo HK: maybe provide the possibility to add up run times from loaded
    !!          data instead of resetting it.
    if (present(clock_start)) then
      me%clock_start = clock_start
    else
      me%clock_start = MPI_Wtime()
    end if

  end subroutine tem_time_load
  ! **************************************************************************** !


  ! **************************************************************************** !
  !> Write the time into a Lua table
  !!
  !! Entries, which are set to the maximal represantable values (meaning never),
  !! are ignored and not written.
  subroutine tem_time_out(me, conf, key)
    ! --------------------------------------------------------------------------!
    !> The time descritpion to write out as Lua table.
    type(tem_time_type), intent(in) :: me

    !> Output handle for the script to write to.
    type(aot_out_type), intent(inout) :: conf

    !> Name for the tabel to write this time description to.
    character(len=*), intent(in), optional :: key
    ! --------------------------------------------------------------------------!
    character(len=labelLen) :: loc_key
    ! --------------------------------------------------------------------------!
    if (present(key)) then
      loc_key = trim(key)
    else
      loc_key = 'time'
    end if

    call aot_out_open_table( put_conf = conf, tname = loc_key )
    if (me%sim < huge(me%sim)) then
      call aot_out_val( put_conf = conf,   &
        &               val      = me%sim, &
        &               vname    = 'sim'   )
    end if
    if (me%iter < huge(me%iter)) then
      call aot_out_val( put_conf = conf,    &
        &               val      = me%iter, &
        &               vname    = 'iter'   )
    end if
    if (me%clock < huge(me%clock)) then
      call aot_out_val( put_conf = conf,     &
        &               val      = me%clock, &
        &               vname    = 'clock'   )
    end if
    call aot_out_close_table( put_conf = conf )

  end subroutine tem_time_out
  ! **************************************************************************** !


  ! **************************************************************************** !
  !> Dump the given time to outUnit.
  !!
  !! Values which are not set, are omitted.
  subroutine tem_time_dump(me, outUnit)
    ! --------------------------------------------------------------------------!
    !> The time that should be written to outunit.
    type(tem_time_type), intent(inout) :: me

    !> The unit to write to.
    integer, intent(in) :: outUnit
    ! --------------------------------------------------------------------------!

    if (me%iter  < huge(me%iter))  write(outUnit, *) ' iterations: ', me%iter
    if (me%sim   < huge(me%sim))   write(outUnit, *) ' simTime   : ', me%sim
    if (me%clock < huge(me%clock)) write(outUnit, *) ' wallClock : ', me%clock

  end subroutine tem_time_dump
  ! **************************************************************************** !


  ! ************************************************************************** !
  !> Function to find a default for time, if it is not defined already.
  !!
  !! Depending on dependency set the value in time to a zero time,
  !! if it is set to never.
  !! This is useful to find a suitable minimum, where an interval is
  !! defined in the timeControl, but no minimum.
  pure function tem_time_default_zero(time, dependency) result(zeroed)
    ! -------------------------------------------------------------------------!
    !> The time to find a default 0 for, if not define, but dependency is.
    type(tem_time_type), intent(in) :: time

    !> A time definition where we set a 0 default in time for, if the
    !! corresponding component is defined, but the time component is
    !! not.
    type(tem_time_type), intent(in) :: dependency

    !> Resulting time, with zeroed components where dependency is defined,
    !! but time not.
    type(tem_time_type) :: zeroed
    ! -------------------------------------------------------------------------!

    zeroed = time

    if (.not. time%sim < huge(time%sim)) then
      ! Time set to never.
      if (dependency%sim < huge(dependency%sim)) then
        ! But dependency not!
        zeroed%sim = 0.0_rk
      end if
    end if
    if (.not. time%iter < huge(time%iter)) then
      ! Time set to never.
      if (dependency%iter < huge(dependency%iter)) then
        ! But dependency not!
        zeroed%iter = 0
      end if
    end if
    if (.not. time%clock < huge(time%clock)) then
      ! Time set to never.
      if (dependency%clock < huge(dependency%clock)) then
        ! But dependency not!
        zeroed%clock = 0.0_rk
      end if
    end if

  end function tem_time_default_zero
  ! ************************************************************************** !


  ! **************************************************************************** !
  !> Return the (>=) comparison of each time measurement between the two
  !! arguments.
  !!
  !! This returns an array of logicals with the length tem_time_n_ids, with
  !! each one indicating the comparison result for the individual measures.
  pure function tem_time_ge_each(time, trigger) result(ge)
    ! --------------------------------------------------------------------------!
    !> A fully defined time definition (all components should have meaningful
    !! settings).
    type(tem_time_type), intent(in) :: time

    !> A comparison time definition, where some entries might be set to never.
    !! If any of the time components is larger, the result will be true.
    type(tem_time_type), intent(in) :: trigger

    !> Result of the comparison for each of the time specifications.
    logical :: ge(tem_time_n_ids)
    ! --------------------------------------------------------------------------!

    ge(tem_time_sim_id)   = (time%sim   >= trigger%sim)
    ge(tem_time_iter_id)  = (time%iter  >= trigger%iter)
    ge(tem_time_clock_id) = (time%clock >= trigger%clock)

  end function tem_time_ge_each
  ! **************************************************************************** !


  ! **************************************************************************** !
  !> Compare (>=) a complete time defintion against a trigger.
  !!
  !! This returns true, if any of the time definitions in time is greater or
  !! equal to the corresponding time given in trigger.
  !! The time argument should be completely defined, while the trigger might
  !! have some of its definitions set to never.
  !! Due to this definition this comparison operator is not useful to define
  !! a unique ordering of time definitions!
  elemental function tem_time_ge_trigger(time, trigger) result(ge)
    ! --------------------------------------------------------------------------!
    !> A fully defined time definition (all components should have meaningful
    !! settings).
    type(tem_time_type), intent(in) :: time

    !> A comparison time definition, where some entries might be set to never.
    !! If any of the time components is larger, the result will be true.
    type(tem_time_type), intent(in) :: trigger

    !> Result of the comparison, true if any of the time specifications in time
    !! is larger or equal to the ones in trigger.
    logical :: ge
    ! --------------------------------------------------------------------------!

    ge = (      (time%sim   >= trigger%sim)   &
      &    .or. (time%iter  >= trigger%iter)  &
      &    .or. (time%clock >= trigger%clock) )

  end function tem_time_ge_trigger
  ! **************************************************************************** !


  ! **************************************************************************** !
  !> Compare (>) a complete time defintion against a trigger.
  !!
  !! This returns true, if any of the time definitions in time is greater
  !! than the corresponding time given in trigger.
  !! The time argument should be completely defined, while the trigger might
  !! have some of its definitions set to never.
  !! Due to this definition this comparison operator is not useful to define
  !! a unique ordering of time definitions!
  elemental function tem_time_gt_trigger(time, trigger) result(gt)
    ! --------------------------------------------------------------------------!
    !> A fully defined time definition (all components should have meaningful
    !! settings).
    type(tem_time_type), intent(in) :: time

    !> A comparison time definition, where some entries might be set to never.
    !! If any of the time components is larger, the result will be true.
    type(tem_time_type), intent(in) :: trigger

    !> Result of the comparison, true if any of the time specifications in time
    !! is larger or equal to the ones in trigger.
    logical :: gt
    ! --------------------------------------------------------------------------!

    gt = (      (time%sim   > trigger%sim)   &
      &    .or. (time%iter  > trigger%iter)  &
      &    .or. (time%clock > trigger%clock) )

  end function tem_time_gt_trigger
  ! **************************************************************************** !


  ! **************************************************************************** !
  !> Add two time definitions to each other (provides the plus operator).
  !!
  !! Entries set to huge (never) will be kept as this, all other entries are
  !! simply added to each other.
  elemental function tem_time_add_time(left, right) result(res)
    ! --------------------------------------------------------------------------!
    !> First operant in the addition of times.
    type(tem_time_type), intent(in) :: left

    !> Second operant in the addition of times.
    type(tem_time_type), intent(in) :: right

    !> Resulting sum of left and right.
    type(tem_time_type) :: res
    ! --------------------------------------------------------------------------!
    real(kind=rk) :: nvrReal
    integer :: nvrInt
    ! --------------------------------------------------------------------------!

    nvrReal = huge(left%sim)
    nvrInt = huge(left%iter)

    if (       (left%sim  < nvrReal)            &
      &  .and. (right%sim < (nvrReal-left%sim)) ) then
      res%sim = left%sim + right%sim
    else
      res%sim = nvrReal
    end if

    if (       (left%iter  < nvrInt)             &
      &  .and. (right%iter < (nvrInt-left%iter)) ) then
      res%iter = left%iter + right%iter
    else
      res%iter = nvrInt
    end if

    if (       (left%clock  < nvrReal)              &
      &  .and. (right%clock < (nvrReal-left%clock)) ) then
      res%clock = left%clock + right%clock
    else
      res%clock = nvrReal
    end if

  end function tem_time_add_time
  ! ************************************************************************** !


  ! ************************************************************************** !
  !> Subtract right time definition from left (provides the minus operator).
  !!
  !! Entries set to huge (never) will be kept as this, all other entries are
  !! simply subtracted.
  elemental function tem_time_subtract_time(left, right) result(res)
    ! -------------------------------------------------------------------------!
    !> First operant in the addition of times.
    type(tem_time_type), intent(in) :: left

    !> Second operant in the addition of times.
    type(tem_time_type), intent(in) :: right

    !> Resulting sum of left and right.
    type(tem_time_type) :: res
    ! -------------------------------------------------------------------------!
    real(kind=rk) :: nvrReal
    integer :: nvrInt
    ! -------------------------------------------------------------------------!

    nvrReal = huge(left%sim)
    nvrInt = huge(left%iter)

    if ( (left%sim  < nvrReal) ) then
      if (right%sim < nvrReal) then
         res%sim = left%sim - right%sim
      else
         res%sim = left%sim
      end if
    else
      res%sim = nvrReal
    end if

    if ( (left%iter  < nvrInt) ) then
      if (right%iter < nvrInt) then
         res%iter = left%iter - right%iter
      else
         res%iter = left%iter
      end if
    else
      res%iter = nvrInt
    end if

    if ( (left%clock  < nvrReal) ) then
      if (right%clock < nvrReal) then
         res%clock = left%clock - right%clock
      else
         res%clock = left%clock
      end if
    else
      res%clock = nvrReal
    end if

  end function tem_time_subtract_time
  ! ************************************************************************** !


  ! ************************************************************************** !
  !> Returns the last full interval before now.
  !!
  !! If interval is 0 or smaller, the result is put to 0. This holds for
  !! time component individually.
  elemental function tem_time_last_interval(now, interval) result(last)
    type(tem_time_type), intent(in) :: now
    type(tem_time_type), intent(in) :: interval
    type(tem_time_type) :: last


    last%sim   = 0.0_rk
    last%iter  = 0
    last%clock = 0.0_rk

    if (interval%sim > 0.0_rk) then
      last%sim = floor(now%sim / interval%sim) * interval%sim
    end if
    if (interval%iter > 0) then
      last%iter = (now%iter / interval%iter) * interval%iter
    end if
    if (interval%clock > 0.0_rk) then
      last%clock = floor(now%clock / interval%clock) * interval%clock
    end if
  end function tem_time_last_interval
  ! ************************************************************************** !


  ! ************************************************************************** !
  !> Get the maximum of two time definitions.
  !!
  !! This is simply the maximum in each component.
  elemental function tem_time_max(left, right) result(res)
    ! --------------------------------------------------------------------------!
    !> First time operant to compare.
    type(tem_time_type), intent(in) :: left

    !> Second time operant to compare.
    type(tem_time_type), intent(in) :: right

    !> Resulting maximum time of left and right.
    type(tem_time_type) :: res
    ! --------------------------------------------------------------------------!

    res%sim   = max(left%sim, right%sim)
    res%iter  = max(left%iter, right%iter)
    res%clock = max(left%clock, right%clock)

  end function tem_time_max
  ! **************************************************************************** !


  ! **************************************************************************** !
  !> Reset the time definition.
  !!
  !! All counters are reset to 0, and the starting clock is set to the current
  !! time.
  subroutine tem_time_reset(me)
    ! --------------------------------------------------------------------------!
    !> Time type to reset
    type(tem_time_type), intent(out) :: me
    ! --------------------------------------------------------------------------!

    me%sim   = 0.0_rk
    me%iter  = 0
    me%clock = 0.0_rk

    me%clock_start = MPI_Wtime()

  end subroutine tem_time_reset
  ! **************************************************************************** !


  ! **************************************************************************** !
  !> Update the clock measurement in the time description.
  !!
  !! The clock component in me is updated with the help of MPI_Wtime.
  subroutine tem_time_set_clock(me)
    ! --------------------------------------------------------------------------!
    !> Time setting to update.
    type(tem_time_type), intent(inout) :: me
    ! --------------------------------------------------------------------------!

    me%clock = MPI_Wtime() - me%clock_start

  end subroutine tem_time_set_clock
  ! **************************************************************************** !


  ! **************************************************************************** !
  !> Advance the time definition.
  !!
  !! Advance the time object by the given simulation time difference.
  !! Optionally, iterations might be advanced by more than one. If iter is not
  !! provided, iterations will be advanced by one.
  !! The running time is always automatically updated using MPI_Wtime.
  !! \note This is only valid, if me has been properly set by tem_time_reset!
  subroutine tem_time_advance(me, sim_dt, iter)
    ! --------------------------------------------------------------------------!
    !> Time definition to advance
    type(tem_time_type), intent(inout) :: me

    !> Increment in simulation time to add
    real(kind=rk) :: sim_dt

    !> If the number of iterations should not be increased by one,
    !! this optional parameter can be used to define the increment for the
    !! iterations. Default: 1.
    integer, optional :: iter
    ! --------------------------------------------------------------------------!
    integer :: iter_inc
    ! --------------------------------------------------------------------------!

    iter_inc = 1
    if (present(iter)) iter_inc = iter

    me%sim   = me%sim + sim_dt
    me%iter  = me%iter + iter_inc
    me%clock = MPI_Wtime() - me%clock_start

  end subroutine tem_time_advance
  ! **************************************************************************** !


  ! **************************************************************************** !
  !> Representation of a point in time, that should never happen.
  function tem_time_never()
    ! --------------------------------------------------------------------------!
    !> A time that represents never in all definitions.
    type(tem_time_type) :: tem_time_never
    ! --------------------------------------------------------------------------!

    tem_time_never%sim   = huge(tem_time_never%sim)
    tem_time_never%iter  = huge(tem_time_never%iter)
    tem_time_never%clock = huge(tem_time_never%clock)

  end function tem_time_never
  ! **************************************************************************** !


  ! **************************************************************************** !
  !> This function returns, if the given time definition requires a reduction
  !! operation if used as a trigger.
  !!
  !! If there is a clock setting involved, a reduction is needed, as the
  !! wtimes are not synchronous across all processes.
  elemental function tem_time_needs_reduce(me) result(needs_reduce)
    ! --------------------------------------------------------------------------!
    !> Time definition to check of its need of a reduction.
    type(tem_time_type), intent(in) :: me

    !> Flag indicating, if this time setting requires a reduction.
    logical :: needs_reduce
    ! --------------------------------------------------------------------------!

    needs_reduce = me%clock < huge(me%clock)

  end function tem_time_needs_reduce
  ! **************************************************************************** !


  ! **************************************************************************** !
  !> This function returns true if tem_time_type is defined either as iter,
  !! sim or clock.
  pure function tem_time_isDefined(me) result(isDefined)
    type(tem_time_type), intent(in) :: me
    logical :: isDefined

    isDefined = (      (me%iter < huge(me%iter)) &
      &           .or. (me%sim < huge(me%sim))   &
      &           .or. (me%clock < huge(me%clock)) )

  end function tem_time_isDefined
  ! **************************************************************************** !


  ! **************************************************************************** !
  !> Generate a time stamp from the simulation time in the given time
  !! definition.
  !!
  !! This basically generates a string identifying the solution time and
  !! writing it in a meaningful format, so that it can be easily recognized.
  function tem_time_sim_stamp(time) result(timeStamp)
    ! --------------------------------------------------------------------------!
    !> Time definition to create the stamp off.
    type(tem_time_type), intent(in) :: time

    !> String representation of the given simulation time.
    character(len=12) :: timeStamp
    ! --------------------------------------------------------------------------!

    write(timeStamp, '(EN12.3)') time%sim

    ! remove leading empty spaces in the timestamp
    timeStamp = adjustl(timeStamp)

  end function  tem_time_sim_stamp
  ! **************************************************************************** !


  ! **************************************************************************** !
  !> Generate a time stamp from the iteration in the given time
  !! definition.
  !!
  !! This basically generates a string identifying the solution time and
  !! writing it in a meaningful format, so that it can be easily recognized.
  function tem_time_iter_stamp(time) result(timeStamp)
    ! --------------------------------------------------------------------------!
    !> Time definition to create the stamp off.
    type(tem_time_type), intent(in) :: time

    !> String representation of the given simulation time.
    character(len=labelLen) :: timeStamp
    ! --------------------------------------------------------------------------!

    write(timeStamp, '(I0)') time%iter

    ! remove leading empty spaces in the timestamp
    timeStamp = adjustl(timeStamp)

  end function  tem_time_iter_stamp
  ! **************************************************************************** !

end module tem_time_module
