module RSpec
  module Core
    # @private
    module Ordering
      # @private
      # The default global ordering (defined order).
      class Identity
        def order(items)
          items
        end
      end

      # @private
      # Orders items randomly.
      class Random
        def initialize(configuration)
          @configuration = configuration
          @used = false
        end

        def used?
          @used
        end

        def order(items)
          @used = true

          seed = @configuration.seed.to_s
          items.sort_by { |item| jenkins_hash_digest(seed + item.id) }
        end

      private

        # http://en.wikipedia.org/wiki/Jenkins_hash_function
        # Jenkins provides a good distribution and is simpler than MD5.
        # It's a bit slower than MD5 (primarily because `Digest::MD5` is
        # implemented in C) but has the advantage of not requiring us
        # to load another part of stdlib, which we try to minimize.
        def jenkins_hash_digest(string)
          hash = 0

          string.each_byte do |byte|
            hash += byte
            hash &= MAX_32_BIT
            hash += ((hash << 10) & MAX_32_BIT)
            hash &= MAX_32_BIT
            hash ^= hash >> 6
          end

          hash += ((hash << 3) & MAX_32_BIT)
          hash &= MAX_32_BIT
          hash ^= hash >> 11
          hash += ((hash << 15) & MAX_32_BIT)
          hash &= MAX_32_BIT
          hash
        end

        MAX_32_BIT = 4_294_967_295
      end

      # @private
      # Orders items by modification time (most recent modified first).
      class RecentlyModified
        def order(list)
          list.sort_by { |item| -File.mtime(item.metadata[:absolute_file_path]).to_i }
        end
      end

      # @private
      # Orders items based on a custom block.
      class Custom
        def initialize(callable)
          @callable = callable
        end

        def order(list)
          @callable.call(list)
        end
      end

      # @private
      # A strategy which delays looking up the ordering until needed
      class Delayed
        def initialize(registry, name)
          @registry = registry
          @name = name
        end

        def order(list)
          strategy.order(list)
        end

        private

        def strategy
          @strategy ||= lookup_strategy
        end

        def lookup_strategy
          raise "Undefined ordering strategy #{@name.inspect}" unless @registry.has_strategy?(@name)
          @registry.fetch(@name)
        end
      end

      # @private
      # Stores the different ordering strategies.
      class Registry
        def initialize(configuration)
          @configuration = configuration
          @strategies    = {}

          register(:random, Random.new(configuration))
          register(:recently_modified, RecentlyModified.new)

          identity = Identity.new
          register(:defined, identity)

          # The default global ordering is --defined.
          register(:global, identity)
        end

        def fetch(name, &fallback)
          @strategies.fetch(name, &fallback)
        end

        def has_strategy?(name)
          @strategies.key?(name)
        end

        def register(sym, strategy)
          @strategies[sym] = strategy
        end

        def used_random_seed?
          @strategies[:random].used?
        end
      end

      # @private
      # Manages ordering configuration.
      #
      # @note This is not intended to be used externally. Use
      #       the APIs provided by `RSpec::Core::Configuration` instead.
      class ConfigurationManager
        attr_reader :seed, :ordering_registry

        def initialize
          @ordering_registry = Registry.new(self)
          @seed = rand(0xFFFF)
          @seed_forced = false
          @order_forced = false
        end

        def seed_used?
          ordering_registry.used_random_seed?
        end

        def seed=(seed)
          return if @seed_forced
          register_ordering(:global, ordering_registry.fetch(:random))
          @seed = seed.to_i
        end

        def order=(type)
          order, seed = type.to_s.split(':')
          @seed = seed.to_i if seed

          ordering_name = if order.include?('rand')
                            :random
                          elsif order == 'defined'
                            :defined
                          elsif order == 'recently-modified'
                            :recently_modified
                          else
                            order.to_sym
                          end

          if ordering_name
            strategy =
              if ordering_registry.has_strategy?(ordering_name)
                ordering_registry.fetch(ordering_name)
              else
                Delayed.new(ordering_registry, ordering_name)
              end

            register_ordering(:global, strategy)
          end
        end

        def force(hash)
          if hash.key?(:seed)
            self.seed = hash[:seed]
            @seed_forced  = true
            @order_forced = true
          elsif hash.key?(:order)
            self.order = hash[:order]
            @order_forced = true
          end
        end

        def register_ordering(name, strategy=Custom.new(Proc.new { |l| yield l }))
          return if @order_forced && name == :global
          ordering_registry.register(name, strategy)
        end
      end
    end
  end
end
