require 'perpetuity/attribute_set'
require 'perpetuity/attribute'
require 'perpetuity/data_injectable'
require 'perpetuity/dereferencer'
require 'perpetuity/retrieval'
require 'perpetuity/dirty_tracker'

module Perpetuity
  class Mapper
    include DataInjectable
    attr_reader :mapper_registry, :identity_map, :dirty_tracker
    class << self
      attr_accessor :collection_name
    end

    def initialize registry=Perpetuity.mapper_registry, id_map=IdentityMap.new
      @mapper_registry = registry
      @identity_map = id_map
      @dirty_tracker = DirtyTracker.new
    end

    def self.map klass, registry=Perpetuity.mapper_registry
      registry[klass] = self
      @mapped_class = klass
      collection klass.name
    end

    def self.attribute_set
      @attribute_set ||= AttributeSet.new
    end

    def self.attribute name, options = {}
      type = options.fetch(:type) { nil }
      attribute_set << Attribute.new(name, type, options)
    end

    def self.attributes
      attribute_set.map(&:name)
    end

    def self.index attribute_names, options={}
      attributes = Array(attribute_names).map { |name| attribute_set[name] }
      if attributes.one?
        data_source.index collection_name, attributes.first, options
      else
        data_source.index collection_name, attributes, options
      end
    end

    def remove_index! index
      data_source.remove_index index
    end

    def indexes
      data_source.indexes(collection_name)
    end

    def reindex!
      indexes.each { |index| data_source.activate_index! index }
      unspecified_indexes.each do |index|
        data_source.remove_index index
      end
    end

    def unspecified_indexes
      active_indexes = data_source.active_indexes(collection_name)
      active_but_unspecified_indexes = (active_indexes - indexes)
      active_but_unspecified_indexes.reject { |index| index.attribute =~ /id/ }
    end

    def attributes
      self.class.attributes
    end

    def attribute_set
      self.class.attribute_set
    end

    def delete_all
      data_source.delete_all collection_name
    end

    def insert object
      objects = Array(object)
      serialized_objects = objects.map { |obj| serialize(obj) }

      new_ids = data_source.insert(collection_name, serialized_objects, attribute_set)
      objects.each_with_index do |obj, index|
        give_id_to obj, new_ids[index]
      end

      if object.is_a? Array
        new_ids
      else
        new_ids.first
      end
    end

    def generate_id_for object
      object.instance_exec(&self.class.id)
    end

    def self.data_source(configuration=Perpetuity.configuration)
      configuration.data_source
    end

    def count &block
      data_source.count collection_name, &block
    end

    def any? &block
      count(&block) > 0
    end

    def all? &block
      count(&block) == count
    end

    def one? &block
      count(&block) == 1
    end

    def none? &block
      !any?(&block)
    end

    def first
      retrieve.limit(1).first
    end

    def all
      retrieve
    end

    def select &block
      retrieve data_source.query(&block)
    end

    alias :find_all :select

    def find id=nil, &block
      return select(&block).first if block_given?

      result = if id.is_a? Array
                 find_all_by_ids id
               else
                 identity_map[mapped_class, id] ||
                 select { |object| object.id == id }.first
               end

      Array(result).each do |r|
        identity_map << r
        dirty_tracker << r
      end

      result
    end

    alias :detect :find

    def find_all_by_ids ids
      ids_in_map    = ids & identity_map.ids_for(mapped_class)
      ids_to_select = ids - ids_in_map
      retrieved     = if ids_to_select.any?
                        select { |object| object.id.in ids_to_select }.to_a
                      else
                        []
                      end
      from_map      = ids_in_map.map { |id| identity_map[mapped_class, id] }

      retrieved.concat from_map
    end

    def reject &block
      retrieve data_source.negate_query(&block)
    end

    def delete object_or_array
      ids = Array(object_or_array).map { |object|
        persisted?(object) ? id_for(object) : object
      }
      data_source.delete ids, collection_name
    end

    def load_association! object, attribute
      objects = Array(object)
      dereferencer = Dereferencer.new(mapper_registry, identity_map)
      dereferencer.load objects.map { |obj| obj.instance_variable_get("@#{attribute}") }

      objects.each do |obj|
        reference = obj.instance_variable_get("@#{attribute}")
        if reference.is_a? Array
          refs = reference
          real_objects = refs.map { |ref| dereferencer[ref] }
          inject_attribute obj, attribute, real_objects
        else
          inject_attribute obj, attribute, dereferencer[reference]
        end
      end
    end

    def self.id type=nil, &block
      if block_given?
        @id = block
        if type
          attribute :id, type: type
        end
        nil
      else
        @id ||= -> { nil }
      end
    end

    def update object, new_data
      id = object.is_a?(mapped_class) ? id_for(object) : object
      data_source.update collection_name, id, new_data
    end

    def save object
      changed_attributes = serialize_changed_attributes(object)
      if changed_attributes && changed_attributes.any?
        update object, changed_attributes
      else
        update object, serialize(object)
      end
    end

    def increment object, attribute, count=1
      id = id_for(object) || object
      data_source.increment collection_name, id, attribute, count
    end

    def decrement object, attribute, count=1
      id = id_for(object) || object
      data_source.increment collection_name, id, attribute, -count
    end

    def sample
      all.sample
    end

    def persisted? object
      !!id_for(object)
    end

    def id_for object
      object.instance_variable_get(:@id)
    end

    def data_source
      self.class.data_source
    end

    def serialize object
      attributes = data_source.serialize(object, self)
      if o_id = generate_id_for(object)
        attributes['id'] = o_id
      end

      attributes
    end

    def serialize_changed_attributes object
      cached = dirty_tracker[object.class, id_for(object)]
      if cached
        data_source.serialize_changed_attributes(object, cached, self)
      end
    end

    def self.mapped_class
      @mapped_class
    end

    def self.collection name
      @collection_name = name.to_s
    end

    def mapped_class
      self.class.mapped_class
    end

    def collection_name
      self.class.collection_name
    end

    private

    def retrieve query=data_source.query
      Perpetuity::Retrieval.new self, query, identity_map: identity_map
    end
  end
end

