module SurfaceMaster
  # Base class for event-based drivers.  Sub-classes should extend the constructor, and implement
  # `respond_to_action`, etc.
  class Interaction
    include Logging

    attr_reader :device, :active

    def initialize(opts = nil)
      opts ||= {}

      self.logger = opts[:logger]
      logger.debug "Initializing #{self.class}##{object_id} with #{opts.inspect}"

      @device       = opts[:device] || @device_class.new(opts.merge(input: true,
                                                                    output: true,
                                                                    logger: opts[:logger]))
      @latency      = (opts[:latency] || 0.001).to_f.abs
      @active       = false
    end

    def change(opts); @device.change(opts); end
    def changes(opts); @device.changes(opts); end

    def close
      logger.debug "Closing #{self.class}##{object_id}"
      stop
      @device.close
    end

    def closed?; @device.closed?; end

    def start
      logger.debug "Starting #{self.class}##{object_id}"

      @active = true
      guard_input_and_reset_at_end! do
        while @active
          @device.read.each { |action| respond_to_action(action) }
          sleep @latency if @latency && @latency > 0.0
        end
      end
    end

    def stop
      logger.debug "Stopping #{self.class}##{object_id}"
      @active = false
    end

    def response_to(types = :all, state = :both, opts = nil, &block)
      logger.debug "Setting response to #{types.inspect} for state #{state.inspect} with"\
        " #{opts.inspect}"
      types   = Array(types)
      opts  ||= {}
      no_response_to(types, state, opts) if opts[:exclusive] == true
      expand_states(state).each do |st|
        add_response_for_state!(types, opts, st, block)
      end
      nil
    end

    def no_response_to(types = nil, state = :both, opts = nil)
      logger.debug "Removing response to #{types.inspect} for state #{state.inspect}"
      types   = Array(types)
      opts  ||= {}
      expand_states(state).each do |st|
        clear_responses_for_state!(types, opts, st)
      end
      nil
    end

    def respond_to(type, state, opts = nil)
      respond_to_action((opts || {}).merge(type: type, state: state))
    end

  protected

    def expand(list)
      list.map { |ll| ll.respond_to?(:to_a) ? ll.to_a : ll }.flatten
    end

    def guard_input_and_reset_at_end!(&block)
      block.call
    rescue Portmidi::DeviceError => e
      logger.fatal "Could not read from device, stopping reader!"
      raise SurfaceMaster::CommunicationError, e
    rescue Exception => e
      logger.fatal "Unkown error, stopping reader: #{e.inspect}"
      raise e
    ensure
      @device.reset!
    end

    def add_response_for_state!(types, opts, state, block)
      response_groups_for(types, opts, state) do |responses|
        responses << block
      end
    end

    def clear_responses_for_state!(types, opts, state)
      response_groups_for(types, opts, state, &:clear)
    end

    def response_groups_for(types, opts, state, &block)
      types.each do |type|
        combined_types(type, opts).each do |combined_type|
          block.call(responses[combined_type][state])
        end
      end
    end

    def expand_states(state); Array(state == :both ? %i(down up) : state); end

    def responses
      # TODO: Generalize for arbitrary actions...
      @responses ||= Hash.new { |hash, key| hash[key] = { down: [], up: [] } }
    end

    def respond_to_action(_action); end
  end
end