# Copyright 2017 Google LLC.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
#    contributors may be used to endorse or promote products derived from this
#    software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""This module implements classes useful for timing how long an action took."""

import time


class TimerError(Exception):
  """Timer Error Exception."""


class Timer(object):
  """Measure time elapsed since some event.

  Example usage:

  from deepvariant.vendor import timer
  a_timer = timer.Timer()
  a_timer.Start()

  # Do stuff here ...

  a_timer.Stop()
  if a_timer.GetDuration() > 100:
    print 'This took too long'

  Another way to use this class is the with statement:

  with timer.Timer() as t:
    # Do time consuming stuff ...
  print 'Time consuming stuff took %f seconds.' % t.GetDuration()
  """

  def __init__(self):
    """Initializes a timer."""
    self.duration = 0
    self.running = False
    self.start = None

  def Start(self):
    """Resets and starts a timer."""

    self._StartInternal()

  def Stop(self):
    """Stops a timer, and records the duration.

    Stop is idempotent.

    Returns:
      The duration, i.e., the time (in seconds) for which the timer ran.
    """
    self._StopInternal()
    return self.duration

  def __enter__(self):
    """Resets and starts a timer.

    This allows Timer to be used as a ContextManager type.

    Returns:
      The object itself so that it can be bound to a variable in a with
      statement.
    """
    self._StartInternal()
    return self

  def __exit__(self, unused_ex_type, unused_ex, unused_ex_trace):
    """Stops a timer and records the duration.

    This allows Timer to be used as a ContextManager type.

    Returns:
      False.  This means that any exceptions raised within the body of the
      caller's with statement will propagate up the stack without interference.
    """
    self._StopInternal()
    return False

  def _StartInternal(self):
    """Resets and starts a timer.

    Common implementation for Start and __enter__.
    """
    self.start = time.time()
    self.running = 1

  def _StopInternal(self):
    """Stops a timer and records the duration.

    Common implementation for Stop and __exit__.
    """
    if self.running:
      self.duration = time.time() - self.start
      self.running = 0

  def IsRunning(self):
    return self.running

  def __str__(self):
    return str(self.GetDuration())

  def GetDuration(self):
    """Returns the elapsed time, in seconds.

    Returns:
       If timer is still running: the time (in seconds) elapsed since the start
       Otherwise: the time (in seconds) for which the timer ran.
    """
    if self.running:
      return time.time() - self.start
    else:
      return self.duration

  def GetStartTime(self):
    """Returns start time of the timer.

    Returns:
       The start time of the timer, floating-point seconds since the epoch.
    Raises:
       TimerError: if the timer was not started
    """
    if self.start:
      return self.start
    else:
      raise TimerError('TimerNotStarted')

  def GetStopTime(self):
    """Returns stop time of the timer.

    Returns:
       The stop time of the timer, floating-point seconds since the epoch.
    Raises:
       TimerError: if the timer was never started or is still running
    """
    if not self.start:
      raise TimerError('TimerNotStarted')
    elif self.running:
      raise TimerError('TimerStillRunning')
    else:
      return self.start + self.duration


class TimerStart(Timer):
  """A timer that automatically starts when you construct it.

  This is otherwise identical in interface to the Timer class in this module.
  """

  def __init__(self):
    Timer.__init__(self)
    self.Start()


class MultiIntervalTimer(Timer):
  """A timer that records cumulative time intervals.

  Example usage:

  from deepvariant.vendor import timer
  a_timer = timer.MultiIntervalTimer()

  # Do stuff the first time
  a_timer.Start()
  # ... stuff ...
  a_timer.Stop()

  # Do stuff the second time (repeat as many times as you like)
  a_timer.Start()
  # ... stuff ...
  a_timer.Stop()

  if a_timer.GetDuration() > 1000:
    print 'Total time spent doing stuff is too much!'

  Another way to use this class is the with statement:

  t = timer.MultiIntervalTimer()
  with t:
    # Do stuff the first time

  with t:
    # Do stuff the second time (repeat as many times as you like)

  print 'Total time spent doing stuff was %f seconds.' % t.GetDuration()
  """

  def __init__(self):
    super(MultiIntervalTimer, self).__init__()
    self.stop = None

  def Reset(self):
    """Resets the measured cumulative time to zero."""
    self.duration = 0

  def _StopInternal(self):
    """Stops a timer and adds the current duration to the total.

    Common implementation for Stop and __exit__.
    """
    if self.running:
      self.stop = time.time()
      self.duration += self.stop - self.start
      self.running = 0

  def GetDuration(self):
    """Returns the total elapsed time, in seconds.

    Returns:
       The total time (in seconds) accumulated so far, regardless of counter
       state (i.e. if stopped, total time spent in all running windows; if
       running, total time spent in all previous running windows plus time
       elapsed from the last call to Start() until GetDuration() was called).
    """
    if self.running:
      return self.duration + (time.time() - self.start)
    else:
      return self.duration

  def GetStopTime(self):
    """Returns the last stop time of the timer.

    Returns:
       The last stop time of the timer, same format as time.time()
    Raises:
       TimerError: if the timer was never started or is still running
    """
    if not self.start:
      return super(MultiIntervalTimer, self).GetStopTime()
    elif self.running:
      return super(MultiIntervalTimer, self).GetStopTime()
    else:
      return self.stop
