require 'chewy/type/adapter/base'

module Chewy
  class Type
    module Adapter
      class Orm < Base
        attr_reader :default_scope

        def initialize *args
          @options = args.extract_options!
          class_or_relation = args.first
          if class_or_relation.is_a?(relation_class)
            @target = model_of_relation(class_or_relation)
            @default_scope = class_or_relation
          else
            @target = class_or_relation
            @default_scope = all_scope
          end
          cleanup_default_scope!
        end

        def name
          @name ||= (options[:name].presence || target.name).to_s.camelize.demodulize
        end

        def identify collection
          if collection.is_a?(relation_class)
            pluck_ids(collection)
          else
            Array.wrap(collection).map do |entity|
              entity.is_a?(object_class) ? entity.public_send(primary_key) : entity
            end
          end
        end

        # Import method for ORM takes import data and import options
        #
        # Import data types:
        #
        #   * Nothing passed - imports all the model data according to type
        #     default scope
        #   * ORM scope
        #   * Objects collection
        #   * Ids collection
        #
        # Import options:
        #
        #   <tt>:batch_size</tt> - import batch size, 1000 objects by default
        #
        # Method handles destroyed objects as well. In case of objects ORM scope
        # or array passed, objects, responding with true to `destroyed?` method will be deleted
        # from index. In case of ids array passed - documents with missing records ids will be
        # deleted from index:
        #
        #   users = User.all
        #   users.each { |user| user.destroy if user.inactive? }
        #   UsersIndex::User.import users # inactive users will be deleted from index
        #   # or
        #   UsersIndex::User.import users.map(&:id) # deleted user ids will be deleted from index
        #
        # Also there is custom type option `delete_if`. It it returns `true`
        # object will be deleted from index. Note that if this option is defined and
        # return `false` Chewy will still check `destroyed?` method. This is useful
        # for paranoid objects deleting implementation.
        #
        #   define_type User, delete_if: ->{ deleted_at } do
        #     ...
        #   end
        #
        #   users = User.all
        #   users.each { |user| user.deleted_at = Time.now }
        #   UsersIndex::User.import users # paranoid deleted users will be deleted from index
        #   # or
        #   UsersIndex::User.import users.map(&:id) # user ids will be deleted from index
        #
        def import *args, &block
          import_options = args.extract_options!
          batch_size = import_options[:batch_size] || BATCH_SIZE

          collection = args.empty? ? default_scope :
            (args.one? && args.first.is_a?(relation_class) ? args.first : args.flatten.compact)

          if collection.is_a?(relation_class)
            import_scope(collection, batch_size, &block)
          else
            import_objects(collection, batch_size, &block)
          end
        end

        def load *args
          load_options = args.extract_options!
          objects = args.flatten

          additional_scope = load_options[load_options[:_type].type_name.to_sym].try(:[], :scope) || load_options[:scope]

          scope = all_scope_where_ids_in(objects.map(&primary_key))
          loaded_objects = if additional_scope.is_a?(Proc)
            scope.instance_exec(&additional_scope)
          elsif additional_scope.is_a?(relation_class) && scope.respond_to?(:merge)
            scope.merge(additional_scope)
          else
            scope
          end.index_by { |object| object.public_send(primary_key).to_s }

          objects.map { |object| loaded_objects[object.public_send(primary_key).to_s] }
        end

      private

        def import_objects(collection, batch_size)
          hash = collection.index_by do |entity|
            entity.is_a?(object_class) ? entity.public_send(primary_key) : entity
          end

          indexed = hash.keys.each_slice(batch_size).map do |ids|
            batch = default_scope_where_ids_in(ids)
            if batch.empty?
              true
            else
              batch.each { |object| hash.delete(object.public_send(primary_key)) }
              yield grouped_objects(batch)
            end
          end.all?

          deleted = hash.keys.each_slice(batch_size).map do |group|
            yield delete: hash.values_at(*group)
          end.all?

          indexed && deleted
        end

        def primary_key
          :id
        end

        def default_scope_where_ids_in(ids)
          scope_where_ids_in(default_scope, ids)
        end

        def all_scope_where_ids_in(ids)
          scope_where_ids_in(all_scope, ids)
        end

        def all_scope
          target.where(nil)
        end

        def model_of_relation relation
          relation.klass
        end
      end
    end
  end
end