# encoding: utf-8
module Mongoid # :nodoc:
  module Relations #:nodoc:

    # This module contains the core macros for defining relations between
    # documents. They can be either embedded or referenced (relational).
    module Macros
      extend ActiveSupport::Concern

      included do
        class_attribute :embedded, :instance_reader => false
        class_attribute :relations
        self.embedded = false
        self.relations = {}
      end

      # This is convenience for librarys still on the old API.
      #
      # @example Get the associations.
      #   person.associations
      #
      # @return [ Hash ] The relations.
      #
      # @since 2.3.1
      def associations
        self.relations
      end

      module ClassMethods #:nodoc:

        # Adds the relation back to the parent document. This macro is
        # necessary to set the references from the child back to the parent
        # document. If a child does not define this relation calling
        # persistence methods on the child object will cause a save to fail.
        #
        # @example Define the relation.
        #
        #   class Person
        #     include Mongoid::Document
        #     embeds_many :addresses
        #   end
        #
        #   class Address
        #     include Mongoid::Document
        #     embedded_in :person
        #   end
        #
        # @param [ Symbol ] name The name of the relation.
        # @param [ Hash ] options The relation options.
        # @param [ Proc ] block Optional block for defining extensions.
        def embedded_in(name, options = {}, &block)
          characterize(name, Embedded::In, options, &block).tap do |meta|
            self.embedded = true
            relate(name, meta)
          end
        end

        # Adds the relation from a parent document to its children. The name
        # of the relation needs to be a pluralized form of the child class
        # name.
        #
        # @example Define the relation.
        #
        #   class Person
        #     include Mongoid::Document
        #     embeds_many :addresses
        #   end
        #
        #   class Address
        #     include Mongoid::Document
        #     embedded_in :person
        #   end
        #
        # @param [ Symbol ] name The name of the relation.
        # @param [ Hash ] options The relation options.
        # @param [ Proc ] block Optional block for defining extensions.
        def embeds_many(name, options = {}, &block)
          characterize(name, Embedded::Many, options, &block).tap do |meta|
            self.cyclic = true if options[:cyclic]
            relate(name, meta)
            validates_relation(meta)
          end
        end

        # Adds the relation from a parent document to its child. The name
        # of the relation needs to be a singular form of the child class
        # name.
        #
        # @example Define the relation.
        #
        #   class Person
        #     include Mongoid::Document
        #     embeds_one :name
        #   end
        #
        #   class Name
        #     include Mongoid::Document
        #     embedded_in :person
        #   end
        #
        # @param [ Symbol ] name The name of the relation.
        # @param [ Hash ] options The relation options.
        # @param [ Proc ] block Optional block for defining extensions.
        def embeds_one(name, options = {}, &block)
          characterize(name, Embedded::One, options, &block).tap do |meta|
            self.cyclic = true if options[:cyclic]
            relate(name, meta)
            builder(name, meta).creator(name, meta)
            validates_relation(meta)
          end
        end

        # Adds a relational association from the child Document to a Document in
        # another database or collection.
        #
        # @example Define the relation.
        #
        #   class Game
        #     include Mongoid::Document
        #     belongs_to :person
        #   end
        #
        #   class Person
        #     include Mongoid::Document
        #     has_one :game
        #   end
        #
        # @param [ Symbol ] name The name of the relation.
        # @param [ Hash ] options The relation options.
        # @param [ Proc ] block Optional block for defining extensions.
        def belongs_to(name, options = {}, &block)
          characterize(name, Referenced::In, options, &block).tap do |meta|
            relate(name, meta)
            reference(meta)
            autosave(meta)
            validates_relation(meta)
          end
        end
        alias :belongs_to_related :belongs_to
        alias :referenced_in :belongs_to

        # Adds a relational association from a parent Document to many
        # Documents in another database or collection.
        #
        # @example Define the relation.
        #
        #   class Person
        #     include Mongoid::Document
        #     has_many :posts
        #   end
        #
        #   class Game
        #     include Mongoid::Document
        #     belongs_to :person
        #   end
        #
        # @param [ Symbol ] name The name of the relation.
        # @param [ Hash ] options The relation options.
        # @param [ Proc ] block Optional block for defining extensions.
        def has_many(name, options = {}, &block)
          characterize(name, Referenced::Many, options, &block).tap do |meta|
            relate(name, meta)
            reference(meta)
            autosave(meta)
            validates_relation(meta)
          end
        end
        alias :has_many_related :has_many
        alias :references_many :has_many

        # Adds a relational many-to-many association between many of this
        # Document and many of another Document.
        #
        # @example Define the relation.
        #
        #   class Person
        #     include Mongoid::Document
        #     has_and_belongs_to_many :preferences
        #   end
        #
        #   class Preference
        #     include Mongoid::Document
        #     has_and_belongs_to_many :people
        #   end
        #
        # @param [ Symbol ] name The name of the relation.
        # @param [ Hash ] options The relation options.
        # @param [ Proc ] block Optional block for defining extensions.
        #
        # @since 2.0.0.rc.1
        def has_and_belongs_to_many(name, options = {}, &block)
          characterize(name, Referenced::ManyToMany, options, &block).tap do |meta|
            relate(name, meta)
            reference(meta, Array)
            autosave(meta)
            validates_relation(meta)
            synced(meta)
          end
        end
        alias :references_and_referenced_in_many :has_and_belongs_to_many

        # Adds a relational association from the child Document to a Document in
        # another database or collection.
        #
        # @example Define the relation.
        #
        #   class Game
        #     include Mongoid::Document
        #     belongs_to :person
        #   end
        #
        #   class Person
        #     include Mongoid::Document
        #     has_one :game
        #   end
        #
        # @param [ Symbol ] name The name of the relation.
        # @param [ Hash ] options The relation options.
        # @param [ Proc ] block Optional block for defining extensions.
        def has_one(name, options = {}, &block)
          characterize(name, Referenced::One, options, &block).tap do |meta|
            relate(name, meta)
            reference(meta)
            builder(name, meta).creator(name, meta).autosave(meta)
            validates_relation(meta)
          end
        end
        alias :has_one_related :has_one
        alias :references_one :has_one

        private

        # Create the metadata for the relation.
        #
        # @example Create the metadata.
        #   Person.characterize(:posts, Referenced::Many, {})
        #
        # @param [ Symbol ] name The name of the relation.
        # @param [ Object ] relation The type of relation.
        # @param [ Hash ] options The relation options.
        # @param [ Proc ] block Optional block for defining extensions.
        #
        # @return [ Metadata ] The metadata for the relation.
        def characterize(name, relation, options, &block)
          Metadata.new({
            :relation => relation,
            :extend => create_extension_module(name, &block),
            :inverse_class_name => self.name,
            :name => name
          }.merge(options))
        end

        # Generate a named extension module suitable for marshaling
        #
        # @example Get the module.
        #   Person.create_extension_module(:posts, &block)
        #
        # @param [ Symbol ] name The name of the relation.
        # @param [ Proc ] block Optional block for defining extensions.
        #
        # @return [ Module, nil ] The extension or nil.
        #
        # @since 2.1.0
        def create_extension_module(name, &block)
          if block
            extension_module_name =
              "#{self.to_s.demodulize}#{name.to_s.camelize}RelationExtension"
            silence_warnings do
              self.const_set(extension_module_name, Module.new(&block))
            end
            "#{self}::#{extension_module_name}".constantize
          end
        end

        # Defines a field to be used as a foreign key in the relation and
        # indexes it if defined.
        #
        # @example Set up the relational fields and indexes.
        #   Person.reference(metadata)
        #
        # @param [ Metadata ] metadata The metadata for the relation.
        def reference(metadata, type = Object)
          polymorph(metadata).cascade(metadata)
          if metadata.relation.stores_foreign_key?
            key = metadata.foreign_key
            field(
              key,
              :type => type,
              :identity => true,
              :metadata => metadata,
              :default => metadata.foreign_key_default
            )
            index(key, :background => true) if metadata.indexed?
          end
        end

        # Creates a relation for the given name, metadata and relation. It adds
        # the metadata to the relations hash and has the accessors set up.
        #
        # @example Set up the relation and accessors.
        #   Person.relate(:addresses, Metadata)
        #
        # @param [ Symbol ] name The name of the relation.
        # @param [ Metadata ] metadata The metadata for the relation.
        def relate(name, metadata)
          self.relations = relations.merge(name.to_s => metadata)
          getter(name, metadata).setter(name, metadata)
        end
      end
    end
  end
end