!======================================
!> Module to provide timing facilities.
!!
!!### Usage
!! The simplest way is using the module variable.
!!
!! Using wall time
!!```fortran
!!call tic()
!!call slow_operation()
!!call toc("slow operation is done")
!!```
!! or CPU time
!!```fortran
!!call cputic()
!!call slow_operation()
!!call cputoc("slow operation is done")
!!```
!!
!! Alternatively, if thread safety is required,
!! you can keep track of the start time yourself.
!!```fortran
!!integer :: start_time
!!call tic(start_time)
!!call slow_operation()
!!call toc("slow operation is done", start_time)
!!```
!! or
!!```fortran
!!real :: start_time
!!call cputic(start_time)
!!call slow_operation()
!!call cputoc("slow operation is done", start_time)
!!```
module mod_timing
  use mod_global_variables, only: dp
  use mod_logging, only: logger, str
  implicit none

  private
  public tic, toc, cputic, cputoc

  !> Wall clock time at last call of `tic()`, init -1000 to easily detect incorrect use.
  integer, save :: wall_clock_tic = -1000
  !> CPU time at last call of `cputic()`, init -1000 to easily detect incorrect use.
  real(dp), save :: cpu_clock_tic = -1000.0_dp

  type, public :: timer_t
    integer, private :: start_time
    integer, private :: program_start_time
    real(dp) :: init_time
    real(dp) :: matrix_time
    real(dp) :: evp_time
    real(dp) :: eigenfunction_time
    real(dp) :: datfile_time

    contains

    procedure :: start_timer
    procedure :: end_timer
    procedure :: get_total_time
  end type timer_t

  public :: new_timer

contains

  function new_timer() result(timer)
    type(timer_t) :: timer
    timer%start_time = -1000
    call system_clock(timer%program_start_time)
  end function new_timer


  subroutine start_timer(this)
    class(timer_t), intent(inout) :: this
    call system_clock(this%start_time)
  end subroutine start_timer


  function end_timer(this) result(elapsed_time)
    class(timer_t), intent(inout) :: this
    integer :: end_time, rate
    real(dp) :: elapsed_time

    call system_clock(end_time, rate)
    elapsed_time = real(end_time - this%start_time, kind=dp) / rate
    this%start_time = -1000
  end function end_timer


  function get_total_time(this) result(total_time)
    class(timer_t), intent(in) :: this
    integer :: end_time, rate
    real(dp) :: total_time

    call system_clock(end_time, rate)
    total_time = real(end_time - this%program_start_time, kind=dp) / rate
  end function get_total_time

  ! LCOV_EXCL_START

  !> Subroutine to start a wall clock timer.
  !!
  !! `tic()` starts the wall clock timer. The subroutines records the wall clock
  !! time at execution of this command. Use `toc(message)` to log the elapsed time
  !! since the last call of `tic()`.
  !!
  !! @warning As `tic()` and `toc(message)` rely on a module variable, they are
  !! not thread safe. @endwarning
  !!
  !! `tic(start_time)` writes the value of the wall clock time at execution
  !! of this command to **start_time**. Calling this command, shall not
  !! change the module variable associated with calling `tic()`.
  subroutine tic(start_time)
    !> Optional output for the start time. If not present the time is
    !! written to a module variable.
    integer, optional, intent(out) :: start_time

    if (present(start_time)) then
        call system_clock(start_time)
    else
        call system_clock(wall_clock_tic)
    end if
  end subroutine tic

  !> Subroutine to end a wall clock timer.
  !!
  !! `toc(message)` logs the elapsed wall clock time in seconds since the
  !! most recent call to `tic()` along with **message** as a debug message.
  !!
  !! @warning As `tic()` and `toc(message)` rely on a module variable, they are
  !! not thread safe. @endwarning
  !!
  !! @warning If `toc(message)` is called without first calling `tic()` the result
  !! will be meaningless. @endwarning
  !!
  !! `toc(message, start_time)` logs the elapsed wall clock time in seconds since
  !! the call of `tic(start_time)`, along with **message**.
  subroutine toc(message, start_time, level)
    !> Message to log along elapsed time.
    character(len=*), intent(in)            :: message
    !> Optional starting time. If not present the time recorded in
    !! the module variable is used.
    integer, intent(in), optional           :: start_time
    !> The level (severity) of the message, default is <tt>"debug"</tt>.
    character(len=*), intent(in), optional  :: level

    integer                   :: selected_start_time, rate, end_time
    real(dp)                  :: elapsed_time
    character(:), allocatable :: loglevel

    if (present(start_time)) then
        selected_start_time = start_time
    else
        selected_start_time = wall_clock_tic
    end if

    if (present(level)) then
      loglevel = level
    else
      loglevel = "debug"
    end if

    call system_clock(end_time, rate)

    elapsed_time = real(end_time - selected_start_time, kind=dp) / rate
    call logger%debug( &
      message // " (" // str(elapsed_time, fmt="f20.3") // " s)" &
    )
  end subroutine toc

  !> Subroutine to start a CPU timer.
  !!
  !! `cputic()` starts the CPU timer. The subroutines records the processor time
  !! at execution of this command. Use `cputoc(message)` to log the elapsed time
  !! since the last call of `cputic()`.
  !!
  !! @warning As `cputic()` and `cputoc(message)` rely on a module variable, they
  !! are not thread safe. @endwarning
  !!
  !! `cputic(start_time)` writes the value of the processor time at execution
  !! of this command to **start_time**. Calling this command, shall not change
  !! the module variable associated with calling `cputic()`.
  subroutine cputic(start_time)
    !> Optional output for the start time. If not present the time is
    !! written to a module variable.
    real, optional, intent(out) :: start_time

    if (present(start_time)) then
        call cpu_time(start_time)
    else
        call cpu_time(cpu_clock_tic)
    end if
  end subroutine cputic

  !> Subroutine to end a CPU timer.
  !!
  !! `cputoc(message)` logs the elapsed CPU time in seconds since the most recent
  !! call to `cputic()` along with **message** as a debug message.
  !!
  !! @warning As `cputic()` and `cputoc(message)` rely on a module variable, they
  !! are not thread safe. @endwarning
  !!
  !! @warning If `cputoc(message)` is called without first calling `cputic()` the
  !! result will be meaningless. @endwarning
  !!
  !! `cputoc(message, start_time)` logs the elapsed CPU time in seconds since the
  !! call of `cputic(start_time)`, along with **message**.
  subroutine cputoc(message, start_time, level)
    !> Message to log along elapsed time.
    character(len=*), intent(in)           :: message
    !> Optional starting time. If not present the time recorded in
    !! the module variable is used.
    real(dp), intent(in), optional :: start_time
    !> The level (severity) of the message, default is <tt>"debug"</tt>.
    character(len=*), intent(in), optional :: level

    real(dp) :: selected_start_time, end_time
    real(dp) :: elapsed_time
    character(:), allocatable :: loglevel

    if (present(start_time)) then
        selected_start_time = start_time
    else
        selected_start_time = cpu_clock_tic
    end if

    if (present(level)) then
      loglevel = level
    else
      loglevel = "debug"
    end if

    call cpu_time(end_time)

    elapsed_time = end_time - selected_start_time
    call logger%debug( &
      message // " (" // str(elapsed_time, fmt="f20.3") // " s, CPU time)" &
    )
  end subroutine cputoc

  ! LCOV_EXCL_END

end module mod_timing
