require 'singleton'

module FakeWeb
  class Registry #:nodoc:
    include Singleton

    attr_accessor :uri_map, :passthrough_uri_map

    def initialize
      clean_registry
    end

    def clean_registry
      self.uri_map = Hash.new { |hash, key| hash[key] = {} }
    end

    def register_uri(method, uri, options)
      uri_map[normalize_uri(uri)][method] = [*[options]].flatten.collect do |option|
        if !option.respond_to?(:keys)
          raise ArgumentError.new("Expected options hash: #{option.inspect}")
        end
        FakeWeb::Responder.new(method, uri, option, option[:times])
      end
    end

    def registered_uri?(method, uri)
      !responders_for(method, uri).empty?
    end

    def response_for(method, uri, &block)
      responders = responders_for(method, uri)
      return nil if responders.empty?

      next_responder = responders.last
      responders.each do |responder|
        if responder.times and responder.times > 0
          responder.times -= 1
          next_responder = responder
          break
        end
      end

      next_responder.response(&block)
    end

    def register_passthrough_uri(uri)
      self.passthrough_uri_map = {normalize_uri(uri) => {:any => true}}
    end

    def remove_passthrough_uri
      self.passthrough_uri_map = {}
    end

    def passthrough_uri_matches?(uri)
      uri = normalize_uri(uri)
      uri_map_matches(passthrough_uri_map, :any, uri, URI) ||
      uri_map_matches(passthrough_uri_map, :any, uri, Regexp)
    end

    private

    def responders_for(method, uri)
      uri = normalize_uri(uri)

      uri_map_matches(uri_map, method, uri, URI) ||
      uri_map_matches(uri_map, :any,   uri, URI) ||
      uri_map_matches(uri_map, method, uri, Regexp) ||
      uri_map_matches(uri_map, :any,   uri, Regexp) ||
      []
    end

    def uri_map_matches(map, method, uri, type_to_check = URI)
      uris_to_check = variations_of_uri_as_strings(uri)

      matches = map.select { |registered_uri, method_hash|
        registered_uri.is_a?(type_to_check) && method_hash.has_key?(method)
      }.select { |registered_uri, method_hash|
        if type_to_check == URI
          uris_to_check.include?(registered_uri.to_s)
        elsif type_to_check == Regexp
          uris_to_check.any? { |u| u.match(registered_uri) }
        end
      }

      if matches.size > 1
        raise MultipleMatchingURIsError,
          "More than one registered URI matched this request: #{method.to_s.upcase} #{uri}"
      end

      matches.map { |_, method_hash| method_hash[method] }.first
    end


    def variations_of_uri_as_strings(uri_object)
      normalized_uri = normalize_uri(uri_object.dup)
      normalized_uri_string = normalized_uri.to_s

      variations = [normalized_uri_string]

      # if the port is implied in the original, add a copy with an explicit port
      if normalized_uri.default_port == normalized_uri.port
        variations << normalized_uri_string.sub(
                        /#{Regexp.escape(normalized_uri.request_uri)}$/,
                        ":#{normalized_uri.port}#{normalized_uri.request_uri}")
      end

      variations
    end

    def normalize_uri(uri)
      return uri if uri.is_a?(Regexp)
      normalized_uri =
        case uri
        when URI then uri
        when String
          uri = 'http://' + uri unless uri.match('^https?://')
          URI.parse(uri)
        end
      normalized_uri.query = sort_query_params(normalized_uri.query)
      normalized_uri.normalize
    end

    def sort_query_params(query)
      if query.nil? || query.empty?
        nil
      else
        query.split('&').sort.join('&')
      end
    end

  end
end
