##################  delay.rb -- brent@mbari.org  #####################
# $Source: /home/cvs/ESP/gen2/software/esp/lib/delay.rb,v $
#    Copyright (C) 2003 MBARI
#    MBARI Proprietary Information. All rights reserved.
# $Id: delay.rb,v 1.21 2005/11/01 10:53:02 brent Exp $
#
#     defines Delay module
#
#     sleep a thread by the specified time or
#     Wake a thread some time in future.
#
#     These functions work by deferring an event that runs 
#     the specified thread at the specified time in the specified Schedule
#
#     sleep causes the thread to block immediately
#     wake does not block the thread.
#     wake is typically followed by another operation that
#     may cause the thread to block, such as reading an event queue.
#
#     These operations work in simulated or real-time.
#
#  Operations include:
#     .sleep (Numeric, Thread, Schedule)  
#       block Thread for specified # of seconds
#
#     .wakeAfter (Numeric, Thread, Schedule) => Event
#       awaken Thread after specified delay
#
#     .sleepUntil (Time|Numeric|String, Thread, Schedule)
#       block until Time
#
#     .wakeAt (Time|Numeric|String, Thread, Schedule) => Event 
#       awaken Thread at Time
#
#     .raiseAt (Time|Numeric|String,Exception|String,Thread,Schedule) => Event
#       raise Exception at specified time in specified Thread on Schedule
#
#     .raiseAfter (Numeric,Exception|String,Thread,Schedule) => Event
#       raise Exception after specified Numeric delay
#
#     .cancel  (Schedule::Event, Schedule) => # of events cancelled (0..1)
#       cancel a previously deferred event
# 
############################################################################

require 'schedule'
require 'timehash'

class Delay < DelegateClass Float

  Minutes = 60
  Hours = 60*Minutes
  Days = 24*Hours  
  Weeks = 7*Days

  class Error < StandardError; end
  
  class RunEvent < Schedule::Event
    def initialize time, thread=Thread.current
      super time
      @thread=thread
    end
    def process
      @thread.run
    end
    attr_reader :thread
  end
  
  class ExceptionEvent < RunEvent
    def initialize time, exception, thread=Thread.current
      super time, thread
      @exception = exception
     end
    def process
      @exception = RuntimeError.new @exception if @exception.is_a? String
      @thread.raise @exception
    end
    attr_reader :thread
  end
  
  #symbolic indices into array returned by Delay#to_a
  Sec=0; Min=1; Hour=2; Day=3; Week=4
       
  @@radix = [60, 60, 24, 7]  #seconds per minute, mins/hr, etc.
  
  def initialize duration
    case duration
      when String
        wholeSecs = (secs = type.parse duration).to_i
        secs = wholeSecs if secs == wholeSecs  #cast to integer if no fraction
      when Array
        mult=1
        secs = duration[0]
        secs = 0 unless secs
        @@radix.each_index {|i|
          mult*=@@radix[i]
          field = duration[i.succ]
          secs += mult*field if field
        }
      else
        secs = +duration
    end
    super secs
  end
 
  def to_a
    type.to_a abs  #abs is the underlying Float # of seconds
  end
  
  def to_s
    s=''
    totalSecs=+self
    (s='-'; totalSecs=-totalSecs) if totalSecs<0
    fraction=totalSecs-(wholeSecs=totalSecs.to_i)
    fraction=('%f'%fraction)
    wholeSecs += 1 if fraction[0]==?1  #in case of .999999
    a=type.to_a wholeSecs  
    fraction.slice! 0
    fraction.chop! while fraction[-1]==?0
    zero = true
    for suffix in [' week',' day'] 
      field = a.pop
      unless field==0
        s<<field.to_s<<suffix
        s<<'s' if field>1
        s<<', '
        zero=false
      end
    end
    if zero or fraction[-1]!=?. or a.detect {|field| field!=0}
      format = '%d'
      for suffix in [':',':','']
        field = a.pop
        unless zero and field==0
          s<<(format%field)<<suffix
          format = '%02d'
          zero=false
        end
      end
      if fraction[-1]!=?.
        s<<fraction
      else
        s<<'0' if zero
      end
    else
      s.chomp! ', '
    end
    s
  end
  alias_method :asIRBtext, :to_s

  def to_str
    result = to_s
    result << " seconds" if self < 60
    result
  end

  def intern
    self  #return a Delay instead of a Float
  end

  def sleep sch=Thread.schedule
    type.sleep self, sch
  end
  
  def delay sch=Thread.schedule
    super self, sch
  end
  
    
  class << self  #Delay class methods 
  
    def to_a secsLeft
    #return array of [seconds, minutes, hours, days, weeks]
    #seconds may be a float, others are integers
    #secsLeft should be a non-negative
      a=[]
      for radix in @@radix
        secsLeft, remainder = secsLeft.divmod radix
        secsLeft = secsLeft.to_i
        a.push remainder
      end
      a.push secsLeft  #days
      a
    end
  

    Parser = Regexp.new ( 
    '\A\s*(-)?\s*(\d*(\.\d*)?\s*W[A-Z]*\s*(,\s*)?)?'+
                '(\d*(\.\d*)?\s*D[A-Z]*\s*(,\s*)?)?'+
       '(\d*(\.\d*)?:)?(\d*(\.\d*)?:)?(\d*(\.\d*)?)\s*\Z')
          
    def parse durationString
    #parse s into a duration on seconds
      raise Error, "Invalid Duration '#{durationString}'" unless
        durationString.upcase[Parser]
#p $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12
      secs = $12.to_f
      secs += Weeks*$2.to_f if $2
      secs += Days*$5.to_f if $5
      if $10  #both hours and minutes specified
        secs += Hours*$8.to_f
        secs += Minutes*$10.to_f
      else   #if only one colon found, assume no hours
        secs += Minutes*$8.to_f if $8
      end
      secs=-secs if $1
      secs
    end
    

    def runAt epochOffset, thread=Thread.current, sch=thread.schedule
    # run thread at specified time expressed as seconds from epoch start
    # return the new Event
      sch.defer ev=RunEvent.new(epochOffset, thread)
      ev
    end

    def runAfter delay, thread=Thread.current, sch=thread.schedule
    # run thread after delay seconds
    # return the new Event
      sch.defer ev=RunEvent.new (sch.now+delay, thread)
      ev
    end

    def sleepUntil time, sch=Thread.schedule
    # stop thread until specified time
      ev=RunEvent.new time
      begin
        sch.defer ev
        Thread.stop
      rescue Exception
        sch.cancel ev
        raise
      end
    end

    def sleep delay=0, sch=Thread.schedule
    # stop thread for delay time units (seconds?)
      sleepUntil (sch.now+delay, sch)
    end

    def raiseAfter (delay, exception,
                                thread=Thread.current, sch=thread.schedule)
    # raise exception in thread after delay time units
    # if exception is a String, raise RuntimeException (string)
    # return the new Event
      sch.defer ev=ExceptionEvent.new (sch.now+delay, exception, thread)
      ev
    end

    def raiseAt (time, exception,
                                thread=Thread.current, sch=thread.schedule)
    # raise exception in thread at specified  time
    # if exception is a String, raise RuntimeException (string)
    # return the new Event
      sch.defer ev=ExceptionEvent.new (time, exception, thread)
      ev
    end

    def cancel event, sch=Thread.schedule
    # remove pending event from sch
    # returns number of events actually removed (0 or 1)
      sch.cancel event
    end
  end
  
end

def delay duration=0, sch=Thread.schedule
  duration=Delay.new duration
  sch.log.recordMethod nil, :delay, [duration]
  duration.sleep sch
end

def delayUntil time, sch=Thread.schedule
  endTime = ParseDate.instant (time, Time.now)
  sch.log.recordMethod nil, :delayUntil, [endTime.asLogString.inspect]
  Delay.sleepUntil endTime, sch
end

class String
  def seconds
    Delay.new self
  end
end

class Numeric
  def seconds
    Delay.new self
  end
end

class Time
  def sleepUntil sch=Thread.schedule
    Delay.sleepUntil self, sch
  end
  def delayUntil sch=Thread.schedule
    sch.log.recordMethod time, :delayUntil
    sleepUntil
  end
end



if $0 == __FILE__
  sch = Schedule.new (100.0)
  ev = nil
  a = Thread (:A) do
    print ("Thread A started @", sch.now, "\n")
    delay (3.14)
    print ("Thread A @", sch.now, "\n")
    ev=Delay.raiseAt (400, "Big, Bad Exception")
    begin
      Thread.stop
    rescue RuntimeError => big
      print "#{Thread.current.name}: @#{sch.now} -- #{big}\n"
    end
    delay (200)
    print ("Thread A done @", sch.now, "\n")    
  end
  b = Thread (:B) do
    print ("Thread B started @", sch.now, "\n")
    delay 5
    print ("Thread B @", sch.now, "\n")
#    sleep 1.5
#    print "Deleted ",Delay.cancel (ev), " events\n"; a.run
    delayUntil (1000)
#    sleep 1
    print ("Thread B done @", sch.now, "\n")
  end
  c = Thread (:C) do
    print ("Thread C started @", sch.now, "\n")
    delay (1.5)
    print ("Thread C @", sch.now, "\n")
    delayUntil (400)
    print ("Thread C done @", sch.now, "\n")
#    sch.terminate
  end
  sch.process
end
