module Listen
  module Event
    class Processor
      def initialize(config, reasons)
        @config = config
        @reasons = reasons
        _reset_no_unprocessed_events
      end

      # TODO: implement this properly instead of checking the state at arbitrary
      # points in time
      def loop_for(latency)
        @latency = latency

        loop do
          _wait_until_events
          _wait_until_events_calm_down
          _wait_until_no_longer_paused
          _process_changes
        end
      rescue Stopped
        Listen::Logger.debug('Processing stopped')
      end

      private

      class Stopped < RuntimeError
      end

      def _wait_until_events_calm_down
        loop do
          now = _timestamp

          # Assure there's at least latency between callbacks to allow
          # for accumulating changes
          diff = _deadline - now
          break if diff <= 0

          # give events a bit of time to accumulate so they can be
          # compressed/optimized
          _sleep(:waiting_until_latency, diff)
        end
      end

      def _wait_until_no_longer_paused
        # TODO: may not be a good idea?
        _sleep(:waiting_for_unpause) while config.paused?
      end

      def _check_stopped
        return unless config.stopped?

        _flush_wakeup_reasons
        raise Stopped
      end

      def _sleep(_local_reason, *args)
        _check_stopped
        sleep_duration = config.sleep(*args)
        _check_stopped

        _flush_wakeup_reasons do |reason|
          next unless reason == :event
          _remember_time_of_first_unprocessed_event unless config.paused?
        end

        sleep_duration
      end

      def _remember_time_of_first_unprocessed_event
        @first_unprocessed_event_time ||= _timestamp
      end

      def _reset_no_unprocessed_events
        @first_unprocessed_event_time = nil
      end

      def _deadline
        @first_unprocessed_event_time + @latency
      end

      def _wait_until_events
        # TODO: long sleep may not be a good idea?
        _sleep(:waiting_for_events) while config.event_queue.empty?
        @first_unprocessed_event_time ||= _timestamp
      end

      def _flush_wakeup_reasons
        reasons = @reasons
        until reasons.empty?
          reason = reasons.pop
          yield reason if block_given?
        end
      end

      def _timestamp
        config.timestamp
      end

      # for easier testing without sleep loop
      def _process_changes
        _reset_no_unprocessed_events

        changes = []
        changes << config.event_queue.pop until config.event_queue.empty?

        callable = config.callable?
        return unless callable

        hash = config.optimize_changes(changes)
        result = [hash[:modified], hash[:added], hash[:removed]]
        return if result.all?(&:empty?)

        block_start = _timestamp
        config.call(*result)
        Listen::Logger.debug "Callback took #{_timestamp - block_start} sec"
      end

      attr_reader :config
    end
  end
end
