# Ruby license. Copyright (C)2004 Joel VanderWerf.
# Contact mailto:vjoel@users.sourceforge.net.
#
# A lightweight, non-drifting, self-correcting timer. Average error is bounded
# as long as, on average, there is enough time to complete the work done, and
# the timer is checked often enough. It is lightweight in the sense that no
# threads are created. Can be used either as an internal iterator (Timer.every)
# or as an external iterator (Timer.new).
#
# Simple usage:
#
# require 'timer'
#
# Timer.every(0.1, 0.5) { |elapsed| puts elapsed }
#
# timer = Timer.new(0.1)
# 5.times do
#   puts timer.elapsed
#   timer.wait
# end 
#
class Timer
  # Yields to the supplied block every +period+ seconds. The value yielded is
  # the total elapsed time (an instance of +Time+). If +expire+ is given, then
  # #every returns after that amount of elapsed time.
  def Timer.every(period, expire = nil)
    target = time_start = Time.now
    loop do
      elapsed = Time.now - time_start
      break if expire and elapsed > expire
      yield elapsed
      target += period
      error = target - Time.now
      sleep error if error > 0
    end
  end
  
  # Make a Timer that can be checked when needed, using #wait or #if_ready. The
  # advantage over Timer.every is that the timer can be checked on separate
  # passes through a loop.
  def initialize(period = 1)
    @period = period
    restart
  end
  
  attr_reader :period
  
  # Call this to restart the timer after a period of inactivity (e.g., the user
  # hits the pause button, and then hits the go button).
  def restart
    @target = @time_start = Time.now
  end
  
  # Time on timer since instantiation or last #restart.
  def elapsed
    Time.now - @time_start
  end
  
  # Wait for the next cycle, if time remains in the current cycle. Otherwise,
  # return immediately to caller.
  def wait
    @target += @period
    error = @target - Time.now
    sleep error if error > 0
    true
  end
  
  # Yield to the block if no time remains in cycle. Otherwise, return
  # immediately to caller
  def if_ready
    error = @target + @period - Time.now
    if error <= 0
      @target += @period
      elapsed = Time.now - @time_start
      yield elapsed
    end
  end
end

if __FILE__ == $0

  require 'test/unit'
  
  # These tests may not work on a slow machine or heavily loaded system; try
  # adjusting FUDGE.
  class Test_Timer < Test::Unit::TestCase # :nodoc:

    FUDGE = 0.01 # a constant independent of period.

    def generic_test
      steps = 100
      period = 0.01
      max_sleep = period/2
      
      start_time = Time.now
      yield steps, period, max_sleep
      finish_time = Time.now
      
      assert_in_delta(
        start_time.to_f + steps*period,
        finish_time.to_f,
        period + FUDGE,
        "delta = #{finish_time.to_f - (start_time.to_f + steps*period)}"
      )
    end

    def test_every
      generic_test do |steps, period, max_sleep|
        Timer.every period do |elapsed|
          s = rand()*max_sleep
          #puts "#{elapsed} elapsed; sleeping #{s}"
          sleep(s)
          steps -= 1
          break if steps == 0
        end
      end
    end
    
    def test_every_with_expire
      generic_test do |steps, period, max_sleep|
        Timer.every period, period*steps do
          s = rand()*max_sleep
          #puts "#{elapsed} elapsed; sleeping #{s}"
          sleep(s)
        end
      end
    end
    
    def test_wait
      generic_test do |steps, period, max_sleep|
        timer = Timer.new(period)
        steps.times do
          s = rand()*max_sleep
          #puts "#{timer.elapsed} elapsed; sleeping #{s}"
          sleep(s)
          timer.wait
        end
      end
    end
  
    def test_if_ready
      generic_test do |steps, period, max_sleep|
        timer = Timer.new(period)
        catch :done do
          loop do
            timer.if_ready do |elapsed|
              s = rand()*max_sleep
              #puts "#{elapsed} elapsed; sleeping #{s}"
              sleep(s)
              steps -= 1
              throw :done if steps == 0
            end
          end
        end
      end
    end
  
  end
  
end