require "thread"

module Stud
  STUD_STOP_REQUESTED = :stud_stop_requested

  # This implementation tries to keep clock more accurately.
  # Prior implementations still permitted skew, where as this one
  # will attempt to correct for skew.
  #
  # The execution patterns of this method should be that
  # the start time of 'block.call' should always be at time T*interval
  def self.interval(time, opts = {}, &block)
    start = Time.now
    while true
      if opts[:sleep_then_run]
        start = sleep_for_interval(time, start)
        break if stop?
        block.call
      else
        block.call
        start = sleep_for_interval(time, start)
        break if stop?
      end
    end # loop forever
  end # def interval

  def interval(time, opts = {}, &block)
    Stud.interval(time, opts, &block)
  end # def interval

  # stop! instructs interval to stop and exit its execution loop before going to
  # sleep between block executions.
  # NOW the tricky part is: this is typically an operation that will be called
  # from another thread than the thread running the interval loop in which case
  # the target parameter must be set to the Thread object which is running the
  # interval loop.
  # Note that the stop logic is compatible with Stud::Task so if interval is run
  # inside a Stud::Task, calling Stud::Task#stop! will stop the interval the same
  # way as calling stop! on the interval itself.
  # @param target [Thread] the target thread to stop, defaut to Thread.current
  def self.stop!(target = Thread.current)
    # setting/getting threalocal var is thread safe in JRuby
    target[STUD_STOP_REQUESTED] = true
    target.wakeup
    nil
  end

  # stop? returns true if stop! has been called
  # @param target [Thread] the target thread to check for stop, defaut to Thread.current
  # @return [Boolean] true if the stop! has been called
  def self.stop?(target = Thread.current)
    # setting/getting threalocal var is thread safe in JRuby
    target[STUD_STOP_REQUESTED]
  end

  class << Stud
    # also support Stud.interrupted? for backward compatibility.
    alias_method :interrupted?, :stop?
  end

  # stoppable_sleep will try to sleep for the given duration seconds (which may be any number,
  # including a Float with fractional seconds). an optional stop_condition_block can be supplied
  # to verify for sleep interruption if the block returns a truthy value. if not block is supplied
  # it will check for the Stud.stop? condition. this check will be performed at 1s interval
  # by default or you can supply a different stop_condition_interval.
  #
  # note that to achieve this, stoppable_sleep will actually perform a series of incremental sleeps
  # but will try accurately spend the requested duration period in the overall stoppable_sleep method call.
  # in other words this means that the duration supplied will be accurate for the time spent in
  # the stoppable_sleep method not the actual total time spent in the underlying multiple sleep calls.
  #
  # @param duration [Numeric] sleep time in (fractional) seconds
  # @param stop_condition_interval [Numeric] optional interval in (fractional) seconds to perform the sleep interruption verification, default is 1s
  # @param stop_condition_block [Proc] optional sleep interruption code block that must evaluate to a truthy value, default is to use Stud.stop?
  # @return [Numeric] the actual duration in (fractional) seconds spent in stoppable_sleep
  def self.stoppable_sleep(duration, stop_condition_interval = 1.0, &stop_condition_block)
    sleep_start = Time.now

    # default to using Stud.stop? as the condition block
    stop_condition_block ||= lambda { stop? }

    while (remaining_duration = (duration - (Time.now - sleep_start))) >= stop_condition_interval
      # sleep X if there is more than X remaining to sleep in relation to the loop start time
      sleep(stop_condition_interval)

      return(Time.now - sleep_start) if stop_condition_block.call
    end

    # here we know we have less than 1s reminding to sleep,
    sleep(remaining_duration) if remaining_duration > 0.0

    Time.now - sleep_start
  end

  private

  def self.sleep_for_interval(time, start)
    duration = Time.now - start

    # sleep only if the duration was less than the time interval
    if duration < time
      stoppable_sleep(time - duration)
      start += time
    else
      # duration exceeded interval time, reset the clock and do not sleep.
      start = Time.now
    end
  end

end # module Stud
