"""
Motivation:

- When creating a long running, looping thread, it needs to be throttled to avoid hammering CPU
- It's often convenient to run the loop at some fixed rate
"""

import time
from typing import Optional


class Rate:
    """
    Usage:
    - Instantiate a Rate object and use rate.sleep() to throttle a loop
    """

    def __init__(self, freq: float):
        self._freq = freq
        self._period = 1.0 / freq
        self._prev_time = time.time()
        self._prev_dt_time = time.time()

    def dt(self) -> float:
        curr_time = time.time()
        dt_ = curr_time - self._prev_dt_time
        self._prev_dt_time = curr_time
        return dt_

    def remaining(self) -> Optional[float]:
        curr_time = time.time()
        dt_ = curr_time - self._prev_time
        if dt_ > self._period:
            self._prev_time = curr_time
            return None
        else:
            return self._period - dt_

    def sleep(self) -> None:
        curr_time = time.time()

        time_passed = curr_time - self._prev_time
        if time_passed > self._period:
            # NOTE (swapnil) - typical usage for Rate.sleep is inside loops. If a particular loop
            # consistently runs over the set rate, then this function will never sleep, and the
            # calling scope will become a "tight loop". Since Python is single-threaded, the OS will
            # struggle to deschedule this type of loop, and it can starve out all other "threads".
            # By adding a tiny sleep, we ensure that the OS has a small window to deschedule this
            # if needed
            time.sleep(0.0001)
            self._prev_time = time.time()
            return
        else:
            time.sleep(self._period - time_passed)
            self._prev_time = time.time()

    def freq(self) -> float:
        return self._freq
    
    def period(self) -> float:
        return self._period
        
    def change_freq(self, freq: float) -> None:
        self._freq = freq
        self._period = 1.0/freq
