module RR
  # RR::Space.instance is the global state subject for the RR framework.
  class Space
    module Reader
      def space
        RR::Space.instance
      end
    end

    class << self
      def instance
        @instance ||= new
      end
      attr_writer :instance

      protected
      if KeywordArguments.fully_supported?
        def method_missing(method_name, *args, **kwargs, &block)
          instance.__send__(method_name, *args, **kwargs, &block)
        end
      else
        def method_missing(method_name, *args, &block)
          instance.__send__(method_name, *args, &block)
        end
        ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
      end
    end

    attr_reader :ordered_doubles, :recorded_calls
    attr_accessor :trim_backtrace

    def initialize
      @ordered_doubles = []
      @trim_backtrace = false
      @recorded_calls = RR::RecordedCalls.new
    end

    # Registers the ordered Double to be verified.
    def register_ordered_double(double)
      @ordered_doubles << double unless ordered_doubles.include?(double)
    end

    # Verifies that the passed in ordered Double is being called
    # in the correct position.
    def verify_ordered_double(double)
      unless double.terminal?
        raise RR::Errors.build_error(:DoubleOrderError,
          "Ordered Doubles cannot have a NonTerminal TimesCalledExpectation")
      end
      unless @ordered_doubles.first == double
        message = Double.formatted_name(double.method_name, double.expected_arguments)
        message << " called out of order in list\n"
        message << Double.list_message_part(@ordered_doubles)
        raise RR::Errors.build_error(:DoubleOrderError, message)
      end
      @ordered_doubles.shift unless double.attempt?
      double
    end

    # Verifies all the DoubleInjection objects have met their
    # TimesCalledExpectations.
    def verify_doubles(*subjects)
      Injections::DoubleInjection.verify(*subjects)
    end
    alias_method :verify, :verify_doubles

    # Resets the registered Doubles and ordered Doubles
    def reset
      RR.trim_backtrace = false
      RR.overridden_error_class = nil
      reset_ordered_doubles
      Injections::DoubleInjection.reset
      reset_method_missing_injections
      reset_singleton_method_added_injections
      reset_recorded_calls
      reset_bound_objects
    end

    # Verifies the DoubleInjection for the passed in subject and method_name.
    def verify_double(subject, method_name)
      Injections::DoubleInjection.verify_double(class << subject; self; end, method_name)
    end

    # Resets the DoubleInjection for the passed in subject and method_name.
    def reset_double(subject, method_name)
      Injections::DoubleInjection.reset_double(class << subject; self; end, method_name)
    end

    def record_call(subject, method_name, arguments, keyword_arguments, block)
      @recorded_calls.add(subject,
                          method_name,
                          arguments,
                          keyword_arguments,
                          block)
    end

    def blank_slate_whitelist
      @blank_slate_whitelist ||= [
        "object_id", "respond_to?", "respond_to_missing?", "method_missing", "instance_eval", "instance_exec", "class_eval"
      ]
    end

  protected
    # Removes the ordered Doubles from the list
    def reset_ordered_doubles
      @ordered_doubles.clear
    end

    def reset_method_missing_injections
      Injections::MethodMissingInjection.instances.each do |subject_class, injection|
        injection.reset
      end
      Injections::MethodMissingInjection.instances.clear
    end

    def reset_singleton_method_added_injections
      Injections::SingletonMethodAddedInjection.instances.each do |subject, injection|
        injection.reset
      end
      Injections::SingletonMethodAddedInjection.instances.clear
    end

    def reset_recorded_calls
      @recorded_calls.clear
    end

    def reset_bound_objects
      # TODO: Figure out how to clear and reset these bindings
      #RR::Injections::DoubleInjection::BoundObjects.clear
      #RR::Injections::DoubleInjection::MethodMissingInjection.clear
    end
  end
end
