# encoding: utf-8
require "mongoid/persistence/atomic"
require "mongoid/persistence/deletion"
require "mongoid/persistence/insertion"
require "mongoid/persistence/modification"
require "mongoid/persistence/operations"

module Mongoid

  # The persistence module is a mixin to provide database accessor methods for
  # the document. These correspond to the appropriate accessors on a
  # mongo collection and retain the same DSL.
  #
  # @example Sample persistence operations.
  #   document.insert
  #   document.update
  #   document.upsert
  module Persistence
    extend ActiveSupport::Concern
    include Atomic

    # Remove the document from the database with callbacks.
    #
    # @example Destroy a document.
    #   document.destroy
    #
    # @param [ Hash ] options Options to pass to destroy.
    #
    # @return [ true, false ] True if successful, false if not.
    def destroy(options = {})
      self.flagged_for_destroy = true
      result = run_callbacks(:destroy) do
        remove(options)
      end
      self.flagged_for_destroy = false
      result
    end

    # Insert a new document into the database. Will return the document
    # itself whether or not the save was successful.
    #
    # @example Insert a document.
    #   document.insert
    #
    # @param [ Hash ] options Options to pass to insert.
    #
    # @return [ Document ] The persisted document.
    def insert(options = {})
      Operations.insert(self, options).persist
    end

    # Remove the document from the database.
    #
    # @example Remove the document.
    #   document.remove
    #
    # @param [ Hash ] options Options to pass to remove.
    #
    # @return [ TrueClass ] True.
    def remove(options = {})
      Operations.remove(self, options).persist
    end
    alias :delete :remove

    # Save the document - will perform an insert if the document is new, and
    # update if not. If a validation error occurs an error will get raised.
    #
    # @example Save the document.
    #   document.save!
    #
    # @param [ Hash ] options Options to pass to the save.
    #
    # @return [ true, false ] True if validation passed.
    def save!(options = {})
      unless upsert(options)
        self.class.fail_validate!(self) if errors.any?
        self.class.fail_callback!(self, :save!)
      end
      return true
    end

    # Touch the document, in effect updating it's updated_at timestamp and
    # optionally the provided field to the current time. If any belongs_to
    # relations exist with a touch option, they will be updated as well.
    #
    # @example Update the updated_at timestamp.
    #   document.touch
    #
    # @example Update the updated_at and provided timestamps.
    #   document.touch(:audited)
    #
    # @note This will not autobuild relations if those options are set.
    #
    # @param [ Symbol ] field The name of an additional field to update.
    #
    # @return [ true ] true.
    #
    # @since 3.0.0
    def touch(field = nil)
      current = Time.now
      write_attribute(:updated_at, current) if fields["updated_at"]
      write_attribute(field, current) if field
      collection.find(atomic_selector).update(atomic_updates)
      without_autobuild do
        touchables.each { |name| send(name).try(:touch) }
      end
      move_changes and true
    end

    # Update the document in the database.
    #
    # @example Update an existing document.
    #   document.update
    #
    # @param [ Hash ] options Options to pass to update.
    #
    # @return [ true, false ] True if succeeded, false if not.
    def update(options = {})
      Operations.update(self, options).persist
    end

    # Update a single attribute and persist the entire document.
    # This skips validation but fires the callbacks.
    #
    # @example Update the attribute.
    #   person.update_attribute(:title, "Sir")
    #
    # @param [ Symbol, String ] name The name of the attribute.
    # @param [ Object ] value The new value of the attribute.a
    #
    # @raise [ Errors::ReadonlyAttribute ] If the field cannot be changed due
    #   to being flagged as reaodnly.
    #
    # @return [ true, false ] True if save was successfull, false if not.
    #
    # @since 2.0.0.rc.6
    def update_attribute(name, value)
      unless attribute_writable?(name.to_s)
        raise Errors::ReadonlyAttribute.new(name, value)
      end
      write_attribute(name, value)
      save(validate: false)
    end

    # Update the document attributes in the database.
    #
    # @example Update the document's attributes
    #   document.update_attributes(:title => "Sir")
    #
    # @param [ Hash ] attributes The attributes to update.
    #
    # @return [ true, false ] True if validation passed, false if not.
    def update_attributes(attributes = {}, options = {})
      assign_attributes(attributes, options); save
    end

    # Update the document attributes in the database and raise an error if
    # validation failed.
    #
    # @example Update the document's attributes.
    #   document.update_attributes(:title => "Sir")
    #
    # @param [ Hash ] attributes The attributes to update.
    #
    # @raise [ Errors::Validations ] If validation failed.
    #
    # @return [ true, false ] True if validation passed.
    def update_attributes!(attributes = {}, options = {})
      result = update_attributes(attributes, options)
      unless result
        self.class.fail_validate!(self) if errors.any?
        self.class.fail_callback!(self, :update_attributes!)
      end
      result
    end

    # Upsert the document - will perform an insert if the document is new, and
    # update if not.
    #
    # @example Upsert the document.
    #   document.upsert
    #
    # @param [ Hash ] options Options to pass to the upsert.
    #
    # @return [ true, false ] True is success, false if not.
    def upsert(options = {})
      if new_record?
        insert(options).persisted?
      else
        update(options)
      end
    end
    alias :save :upsert

    module ClassMethods #:nodoc:

      # Create a new document. This will instantiate a new document and
      # insert it in a single call. Will always return the document
      # whether save passed or not.
      #
      # @example Create a new document.
      #   Person.create(:title => "Mr")
      #
      # @param [ Hash ] attributes The attributes to create with.
      # @param [ Hash ] options A mass-assignment protection options. Supports
      #   :as and :without_protection
      #
      # @return [ Document ] The newly created document.
      def create(attributes = {}, options = {}, &block)
        _creating do
          doc = new(attributes, options, &block)
          doc.save
          doc
        end
      end

      # Create a new document. This will instantiate a new document and
      # insert it in a single call. Will always return the document
      # whether save passed or not, and if validation fails an error will be
      # raise.
      #
      # @example Create a new document.
      #   Person.create!(:title => "Mr")
      #
      # @param [ Hash ] attributes The attributes to create with.
      # @param [ Hash ] options A mass-assignment protection options. Supports
      #   :as and :without_protection
      #
      # @return [ Document ] The newly created document.
      def create!(attributes = {}, options = {}, &block)
        _creating do
          doc = new(attributes, options, &block)
          fail_validate!(doc) if doc.insert.errors.any?
          fail_callback!(doc, :create!) if doc.new_record?
          doc
        end
      end

      # Delete all documents given the supplied conditions. If no conditions
      # are passed, the entire collection will be dropped for performance
      # benefits. Does not fire any callbacks.
      #
      # @example Delete matching documents from the collection.
      #   Person.delete_all(:conditions => { :title => "Sir" })
      #
      # @example Delete all documents from the collection.
      #   Person.delete_all
      #
      # @param [ Hash ] conditions Optional conditions to delete by.
      #
      # @return [ Integer ] The number of documents deleted.
      def delete_all(conditions = nil)
        conds = conditions || {}
        selector = conds[:conditions] || conds
        selector.merge!(_type: name) if hereditary?
        coll = collection
        deleted = coll.find(selector).count
        coll.find(selector).remove_all
        Threaded.clear_options!
        deleted
      end

      # Delete all documents given the supplied conditions. If no conditions
      # are passed, the entire collection will be dropped for performance
      # benefits. Fires the destroy callbacks if conditions were passed.
      #
      # @example Destroy matching documents from the collection.
      #   Person.destroy_all(:conditions => { :title => "Sir" })
      #
      # @example Destroy all documents from the collection.
      #   Person.destroy_all
      #
      # @param [ Hash ] conditions Optional conditions to destroy by.
      #
      # @return [ Integer ] The number of documents destroyed.
      def destroy_all(conditions = nil)
        conds = conditions || {}
        documents = where(conds[:conditions] || conds)
        destroyed = documents.count
        documents.each { |doc| doc.destroy }
        destroyed
      end

      # Raise an error if validation failed.
      #
      # @example Raise the validation error.
      #   Person.fail_validate!(person)
      #
      # @param [ Document ] document The document to fail.
      def fail_validate!(document)
        raise Errors::Validations.new(document)
      end

      # Raise an error if a callback failed.
      #
      # @example Raise the callback error.
      #   Person.fail_callback!(person, :create!)
      #
      # @param [ Document ] document The document to fail.
      # @param [ Symbol ] method The method being called.
      #
      # @since 2.2.0
      def fail_callback!(document, method)
        raise Errors::Callback.new(document.class, method)
      end
    end
  end
end
