# frozen_string_literal: true
# encoding: utf-8
module Mongoid
  module Extensions
    module Hash

      # Evolves each value in the hash to an object id if it is convertable.
      #
      # @example Convert the hash values.
      #   { field: id }.__evolve_object_id__
      #
      # @return [ Hash ] The converted hash.
      #
      # @since 3.0.0
      def __evolve_object_id__
        update_values(&:__evolve_object_id__)
      end

      # Mongoizes each value in the hash to an object id if it is convertable.
      #
      # @example Convert the hash values.
      #   { field: id }.__mongoize_object_id__
      #
      # @return [ Hash ] The converted hash.
      #
      # @since 3.0.0
      def __mongoize_object_id__
        if id = self['$oid']
          BSON::ObjectId.from_string(id)
        else
          update_values(&:__mongoize_object_id__)
        end
      end

      # Consolidate the key/values in the hash under an atomic $set.
      #
      # @example Consolidate the hash.
      #   { name: "Placebo" }.__consolidate__
      #
      # @return [ Hash ] A new consolidated hash.
      #
      # @since 3.0.0
      def __consolidate__(klass)
        consolidated = {}
        each_pair do |key, value|
          if key =~ /\$/
            value.each_pair do |_key, _value|
              value[_key] = (key == "$rename") ? _value.to_s : mongoize_for(key, klass, _key, _value)
            end
            (consolidated[key] ||= {}).merge!(value)
          else
            (consolidated["$set"] ||= {}).merge!(key => mongoize_for(key, klass, key, value))
          end
        end
        consolidated
      end

      # Check if the hash is part of a blank association criteria.
      #
      # @example Is the hash blank criteria?
      #   {}.blank_criteria?
      #
      # @return [ true, false ] If the hash is blank criteria.
      #
      # @since 3.1.0
      def blank_criteria?
        self == { "_id" => { "$in" => [] }}
      end

      # Deletes an id value from the hash.
      #
      # @example Delete an id value.
      #   {}.delete_id
      #
      # @return [ Object ] The deleted value, or nil.
      #
      # @since 3.0.2
      def delete_id
        delete("_id") || delete("id") || delete(:id) || delete(:_id)
      end

      # Get the id attribute from this hash, whether it's prefixed with an
      # underscore or is a symbol.
      #
      # @example Extract the id.
      #   { :_id => 1 }.extract_id
      #
      # @return [ Object ] The value of the id.
      #
      # @since 2.3.2
      def extract_id
        self["_id"] || self["id"] || self[:id] || self[:_id]
      end

      # Fetch a nested value via dot syntax.
      #
      # @example Fetch a nested value via dot syntax.
      #   { "name" => { "en" => "test" }}.__nested__("name.en")
      #
      # @param [ String ] string the dot syntax string.
      #
      # @return [ Object ] The matching value.
      #
      # @since 3.0.15
      def __nested__(string)
        keys = string.split(".")
        value = self
        keys.each do |key|
          return nil if value.nil?
          value_for_key = value[key]
          if value_for_key.nil? && key.to_i.to_s == key
            value_for_key = value[key.to_i]
          end
          value = value_for_key
        end
        value
      end

      # Turn the object from the ruby type we deal with to a Mongo friendly
      # type.
      #
      # @example Mongoize the object.
      #   object.mongoize
      #
      # @return [ Hash ] The object.
      #
      # @since 3.0.0
      def mongoize
        ::Hash.mongoize(self)
      end

      # Can the size of this object change?
      #
      # @example Is the hash resizable?
      #   {}.resizable?
      #
      # @return [ true ] true.
      #
      # @since 3.0.0
      def resizable?
        true
      end

      # Convert this hash to a criteria. Will iterate over each keys in the
      # hash which must correspond to method on a criteria object. The hash
      # must also include a "klass" key.
      #
      # @example Convert the hash to a criteria.
      #   { klass: Band, where: { name: "Depeche Mode" }.to_criteria
      #
      # @return [ Criteria ] The criteria.
      #
      # @since 3.0.7
      def to_criteria
        criteria = Criteria.new(delete(:klass) || delete("klass"))
        each_pair do |method, args|
          criteria = criteria.__send__(method, args)
        end
        criteria
      end

      private

      # Mongoize for the klass, key and value.
      #
      # @api private
      #
      # @example Mongoize for the klass, field and value.
      #   {}.mongoize_for("$push", Band, "name", "test")
      #
      # @param [ String ] operator The operator.
      # @param [ Class ] klass The model class.
      # @param [ String, Symbol ] key The field key.
      # @param [ Object ] value The value to mongoize.
      #
      # @return [ Object ] The mongoized value.
      #
      # @since 3.1.0
      def mongoize_for(operator, klass, key, value)
        field = klass.fields[key.to_s]
        if field
          val = field.mongoize(value)
          if Mongoid::Persistable::LIST_OPERATIONS.include?(operator) && field.resizable?
            val = val.first if !value.is_a?(Array)
          end
          val
        else
          value
        end
      end

      module ClassMethods

        # Turn the object from the ruby type we deal with to a Mongo friendly
        # type.
        #
        # @example Mongoize the object.
        #   Hash.mongoize([ 1, 2, 3 ])
        #
        # @param [ Object ] object The object to mongoize.
        #
        # @return [ Hash ] The object mongoized.
        #
        # @since 3.0.0
        def mongoize(object)
          return if object.nil?
          evolve(object.dup).update_values { |value| value.mongoize }
        end

        # Can the size of this object change?
        #
        # @example Is the hash resizable?
        #   {}.resizable?
        #
        # @return [ true ] true.
        #
        # @since 3.0.0
        def resizable?
          true
        end
      end
    end
  end
end

::Hash.__send__(:include, Mongoid::Extensions::Hash)
::Hash.extend(Mongoid::Extensions::Hash::ClassMethods)
