module God
  module Conditions
    
    # Condition Symbol :flapping
    # Type: Trigger
    # 
    # Trigger when a Task transitions to or from a state or states a given number
    # of times within a given period. 
    #
    # Paramaters
    #   Required
    #     +times+ is the number of times that the Task must transition before
    #             triggering.
    #     +within+ is the number of seconds within which the Task must transition
    #              the specified number of times before triggering. You may use
    #              the sugar methods #seconds, #minutes, #hours, #days to clarify
    #              your code (see examples).
    #     --one or both of--
    #     +from_state+ is the state (as a Symbol) from which the transition must occur.
    #     +to_state is the state (as a Symbol) to which the transition must occur.
    #
    #   Optional:
    #     +retry_in+ is the number of seconds after which to re-monitor the Task after
    #                it has been disabled by the condition.
    #     +retry_times+ is the number of times after which to permanently unmonitor
    #                   the Task.
    #     +retry_within+ is the number of seconds within which 
    #
    # Examples
    #
    # Trigger if
    class Flapping < TriggerCondition
      attr_accessor :times,
                    :within,
                    :from_state,
                    :to_state,
                    :retry_in,
                    :retry_times,
                    :retry_within
      
      def initialize
        self.info = "process is flapping"
      end
      
      def prepare
        @timeline = Timeline.new(self.times)
        @retry_timeline = Timeline.new(self.retry_times)
      end
      
      def valid?
        valid = true
        valid &= complain("Attribute 'times' must be specified", self) if self.times.nil?
        valid &= complain("Attribute 'within' must be specified", self) if self.within.nil?
        valid &= complain("Attributes 'from_state', 'to_state', or both must be specified", self) if self.from_state.nil? && self.to_state.nil?
        valid
      end
      
      def process(event, payload)
        begin
          if event == :state_change
            event_from_state, event_to_state = *payload
            
            from_state_match = !self.from_state || self.from_state && Array(self.from_state).include?(event_from_state)
            to_state_match = !self.to_state || self.to_state && Array(self.to_state).include?(event_to_state)
            
            if from_state_match && to_state_match
              @timeline << Time.now
              
              concensus = (@timeline.size == self.times)
              duration = (@timeline.last - @timeline.first) < self.within
              
              if concensus && duration
                @timeline.clear
                trigger
                retry_mechanism
              end
            end
          end
        rescue => e
          puts e.message
          puts e.backtrace.join("\n")
        end
      end
      
      private
      
      def retry_mechanism
        if self.retry_in
          @retry_timeline << Time.now
          
          concensus = (@retry_timeline.size == self.retry_times)
          duration = (@retry_timeline.last - @retry_timeline.first) < self.retry_within
          
          if concensus && duration
            # give up
            Thread.new do
              sleep 1
              
              # log
              msg = "#{self.watch.name} giving up"
              applog(self.watch, :info, msg)
            end
          else
            # try again later
            Thread.new do
              sleep 1
              
              # log
              msg = "#{self.watch.name} auto-reenable monitoring in #{self.retry_in} seconds"
              applog(self.watch, :info, msg)
              
              sleep self.retry_in
              
              # log
              msg = "#{self.watch.name} auto-reenabling monitoring"
              applog(self.watch, :info, msg)
              
              if self.watch.state == :unmonitored
                self.watch.monitor
              end
            end
          end
        end
      end
    end
    
  end
end