!> The simulation clock: simulation time from wall-clock time, with a time
!> scale, pause and a time range, as in OpenFrames' WindowProxy.
!>
!> While running, time = offset + (wall - wall_ref) * scale, limited to the
!> time range. Setting the time, pausing and changing the scale restart the
!> count from the current time (offset = time, wall_ref = now). A clock can be
!> synchronized to another one ([[synchronize]]), whose time it then reports and
!> whose settings it changes.
!>
!> Wall time is in seconds from `system_clock`, unless given explicitly with the
!> optional `wall` arguments (for tests, or for driving the clock from a host).
module vkf_clock

    use, intrinsic :: iso_fortran_env, only: int64
    use vkf_math, only: wp

    implicit none

    private

    !> A simulation clock (see the module documentation).
    type, public :: SimulationClock
        private
        real(wp) :: time_ = 0.0_wp          !! the current simulation time (s)
        real(wp) :: offset = 0.0_wp         !! simulation time at wall_ref (s)
        real(wp) :: wall_ref = 0.0_wp       !! wall time (s) at which counting restarted
        real(wp) :: scale_ = 1.0_wp         !! simulation seconds per wall second
        real(wp) :: t_min = -huge(1.0_wp)   !! start of the time range (s; default unlimited)
        real(wp) :: t_max = huge(1.0_wp)    !! end of the time range (s; default unlimited)
        logical :: paused_ = .false.        !! true while paused
        logical :: started = .false.        !! false until counting first started (set_time or update)
        type(SimulationClock), pointer :: sync => null()    !! the clock this one is synchronized to (not owned)
    contains
        procedure :: update
        procedure :: time, set_time
        procedure :: set_time_range, time_range
        procedure :: pause, is_paused
        procedure :: set_time_scale, time_scale
        procedure :: synchronize, synchronized_to
    end type SimulationClock

    public :: wall_time

contains

    !> Wall-clock time in seconds (system_clock), for the clock's default.
    real(wp) function wall_time()
        integer(int64) :: count, rate
        call system_clock(count, rate)
        wall_time = real(count, wp) / real(rate, wp)
    end function wall_time

    !> The wall time (s): `wall` if present, else `wall_time()`.
    real(wp) function now(wall)
        real(wp), intent(in), optional :: wall      !! wall time (s)
        if (present(wall)) then
            now = wall
        else
            now = wall_time()
        end if
    end function now

    !> Advance the clock to the current wall time (call once per frame) and
    !> return the simulation time.
    real(wp) function update(self, wall) result(t)
        class(SimulationClock), intent(inout) :: self
        real(wp), intent(in), optional :: wall      !! wall time (s; default: wall_time())
        if (associated(self%sync)) then
            self%time_ = self%sync%time()
        else
            if (.not. self%started) call self%set_time(self%time_, wall)
            if (.not. self%paused_) &
                self%time_ = max(self%t_min, min(self%t_max, self%offset + (now(wall) - self%wall_ref) * self%scale_))
        end if
        t = self%time_
    end function update

    !> The simulation time at the last [[update]] (or as last set).
    recursive real(wp) function time(self)
        class(SimulationClock), intent(in) :: self
        if (associated(self%sync)) then
            time = self%sync%time()
        else
            time = self%time_
        end if
    end function time

    !> Set the simulation time (limited to the time range) and restart counting from it.
    recursive subroutine set_time(self, t, wall)
        class(SimulationClock), intent(inout) :: self
        real(wp), intent(in) :: t                   !! simulation time (s)
        real(wp), intent(in), optional :: wall      !! wall time (s; default: wall_time())
        if (associated(self%sync)) then
            call self%sync%set_time(t, wall)
            return
        end if
        self%time_ = max(self%t_min, min(self%t_max, t))
        self%offset = self%time_
        self%wall_ref = now(wall)
        self%started = .true.
    end subroutine set_time

    !> Limit the simulation time to [t_min, t_max] (ignored if t_max < t_min).
    recursive subroutine set_time_range(self, t_min, t_max)
        class(SimulationClock), intent(inout) :: self
        real(wp), intent(in) :: t_min, t_max        !! the time range (s)
        if (associated(self%sync)) then
            call self%sync%set_time_range(t_min, t_max)
        else if (t_max >= t_min) then
            self%t_min = t_min
            self%t_max = t_max
        end if
    end subroutine set_time_range

    !> The time range the simulation time is limited to (s; +-huge() if unlimited).
    recursive subroutine time_range(self, t_min, t_max)
        class(SimulationClock), intent(in) :: self
        real(wp), intent(out) :: t_min, t_max       !! the time range (s)
        if (associated(self%sync)) then
            call self%sync%time_range(t_min, t_max)
        else
            t_min = self%t_min
            t_max = self%t_max
        end if
    end subroutine time_range

    !> Pause (true) or resume (false) the simulation time.
    recursive subroutine pause(self, paused, wall)
        class(SimulationClock), intent(inout) :: self
        logical, intent(in) :: paused               !! true: pause; false: resume
        real(wp), intent(in), optional :: wall      !! wall time (s; default: wall_time())
        if (associated(self%sync)) then
            call self%sync%pause(paused, wall)
            return
        end if
        self%paused_ = paused
        call self%set_time(self%time_, wall)
    end subroutine pause

    !> True if the simulation time is paused.
    recursive logical function is_paused(self)
        class(SimulationClock), intent(in) :: self
        if (associated(self%sync)) then
            is_paused = self%sync%is_paused()
        else
            is_paused = self%paused_
        end if
    end function is_paused

    !> Simulation seconds per wall-clock second (may be 0 or negative).
    recursive subroutine set_time_scale(self, scale, wall)
        class(SimulationClock), intent(inout) :: self
        real(wp), intent(in) :: scale               !! simulation seconds per wall-clock second
        real(wp), intent(in), optional :: wall      !! wall time (s; default: wall_time())
        if (associated(self%sync)) then
            call self%sync%set_time_scale(scale, wall)
            return
        end if
        self%scale_ = scale
        call self%set_time(self%time_, wall)
    end subroutine set_time_scale

    !> Simulation seconds per wall-clock second.
    recursive real(wp) function time_scale(self)
        class(SimulationClock), intent(in) :: self
        if (associated(self%sync)) then
            time_scale = self%sync%time_scale()
        else
            time_scale = self%scale_
        end if
    end function time_scale

    !> Follow `master`'s time (it must stay alive while synchronized), or stop
    !> following (master not associated, or this clock itself), keeping the
    !> master's current time and settings. Returns false, and changes nothing,
    !> if this would make a cycle.
    recursive logical function synchronize(self, master, wall) result(ok)
        class(SimulationClock), intent(inout), target :: self
        type(SimulationClock), pointer, intent(in) :: master    !! the clock to follow (null: stop following)
        real(wp), intent(in), optional :: wall      !! wall time (s; default: wall_time())
        type(SimulationClock), pointer :: c
        real(wp) :: t
        ok = .true.
        if (.not. associated(master)) then
            if (associated(self%sync)) then
                t = self%sync%time()
                self%scale_ = self%sync%time_scale()
                self%paused_ = self%sync%is_paused()
                call self%sync%time_range(self%t_min, self%t_max)
                self%sync => null()
                call self%set_time(t, wall)
            end if
            return
        end if
        if (same_clock(master, self)) then
            ok = self%synchronize(null(), wall)
            return
        end if
        c => master
        do while (associated(c))
            if (same_clock(c, self)) then
                ok = .false.
                return
            end if
            c => c%sync
        end do
        self%sync => master
    end function synchronize

    !> The clock this one is synchronized to (null if none).
    function synchronized_to(self) result(master)
        class(SimulationClock), intent(in) :: self
        type(SimulationClock), pointer :: master
        master => self%sync
    end function synchronized_to

    !> True if `a` points to the clock `b`.
    logical function same_clock(a, b)
        type(SimulationClock), pointer, intent(in) :: a !! the clock pointer (may be null)
        class(SimulationClock), intent(in), target :: b !! the clock
        select type (b)
        type is (SimulationClock)
            same_clock = associated(a, b)
        class default
            same_clock = .false.
        end select
    end function same_clock

end module vkf_clock
