module Hashie
  module Extensions
    module DeepLocate
      # The module level implementation of #deep_locate, incase you do not want
      # to include/extend the base datastructure. For further examples please
      # see #deep_locate.
      #
      # @example
      #   books = [
      #     {
      #       title: "Ruby for beginners",
      #       pages: 120
      #     },
      #     ...
      #   ]
      #
      #   DeepLocate.deep_locate -> (key, value, object) { key == :title }, books
      #   # => [{:title=>"Ruby for beginners", :pages=>120}, ...]
      def self.deep_locate(comparator, object)
        unless comparator.respond_to?(:call)
          comparator = _construct_key_comparator(comparator, object)
        end

        _deep_locate(comparator, object)
      end

      # Performs a depth-first search on deeply nested data structures for a
      # given comparator callable and returns each Enumerable, for which the
      # callable returns true for at least one the its elements.
      #
      # @example
      #   books = [
      #     {
      #       title: "Ruby for beginners",
      #       pages: 120
      #     },
      #     {
      #       title: "CSS for intermediates",
      #       pages: 80
      #     },
      #     {
      #       title: "Collection of ruby books",
      #       books: [
      #         {
      #           title: "Ruby for the rest of us",
      #           pages: 576
      #         }
      #       ]
      #     }
      #   ]
      #
      #   books.extend(Hashie::Extensions::DeepLocate)
      #
      #   # for ruby 1.9 leave *no* space between the lambda rocket and the braces
      #   # http://ruby-journal.com/becareful-with-space-in-lambda-hash-rocket-syntax-between-ruby-1-dot-9-and-2-dot-0/
      #
      #   books.deep_locate -> (key, value, object) { key == :title && value.include?("Ruby") }
      #   # => [{:title=>"Ruby for beginners", :pages=>120},
      #   #     {:title=>"Ruby for the rest of us", :pages=>576}]
      #
      #   books.deep_locate -> (key, value, object) { key == :pages && value <= 120 }
      #   # => [{:title=>"Ruby for beginners", :pages=>120},
      #   #     {:title=>"CSS for intermediates", :pages=>80}]
      def deep_locate(comparator)
        Hashie::Extensions::DeepLocate.deep_locate(comparator, self)
      end

      def self._construct_key_comparator(search_key, object)
        if object.respond_to?(:indifferent_access?) && object.indifferent_access? ||
           activesupport_indifferent?(object)
          search_key = search_key.to_s
        end

        lambda do |non_callable_object|
          ->(key, _, _) { key == non_callable_object }
        end.call(search_key)
      end
      private_class_method :_construct_key_comparator

      def self._deep_locate(comparator, object, result = [])
        if object.is_a?(::Enumerable)
          if object.any? { |value| _match_comparator?(value, comparator, object) }
            result.push object
          end

          (object.respond_to?(:values) ? object.values : object.entries).each do |value|
            _deep_locate(comparator, value, result)
          end
        end

        result
      end
      private_class_method :_deep_locate

      def self._match_comparator?(value, comparator, object)
        if object.is_a?(::Hash)
          key, value = value
        else
          key = nil
        end

        comparator.call(key, value, object)
      end
      private_class_method :_match_comparator?

      def self.activesupport_indifferent?(object)
        defined?(::ActiveSupport::HashWithIndifferentAccess) &&
          object.is_a?(::ActiveSupport::HashWithIndifferentAccess)
      end
      private_class_method :activesupport_indifferent?
    end
  end
end
