module ActiveShepherd::AggregateRoot
  def self.included(base)
    base.extend(ClassMethods)
  end

  # Private: returns the behind the scenes object that does all the work
  def aggregate
    @aggregate ||= ActiveShepherd::Aggregate.new(self)
  end
  private :aggregate
  
  # Public: Given a serializable blob of changes (Hash, Array, and String)
  # objects, apply those changes to 
  #
  # Examples:
  # 
  #   @project.aggregate_changes = { name: ["Clean House", "Clean My House"] }
  #
  # Returns nothing.
  # Raises ActiveShepherd::InvalidChangesError if the changes supplied do not
  #   pass #valid_aggregate_changes? (see below)
  # Raises ActiveShepherd::BadChangeError if a particular attribute change
  #   cannot be applied.
  # Raises an ActiveShepherd::AggregateMismatchError if any objects in the
  #   aggregate are being asked to change attributes that do not exist.
  def aggregate_changes=(changes)
    changes_errors = ActiveShepherd::ChangesValidator.new(self).validate changes
    unless changes_errors.empty?
      raise ActiveShepherd::InvalidChangesError, "changes hash is invalid: "\
        "#{changes_errors.join(', ')}"
    end
    # The validation process actually runs the changes internally. This means
    # we don't have to explicitly invoke # #apply_changes here.
    # XXX: remove when ChangesValidator does this
    ActiveShepherd::Methods::ApplyChanges.apply_changes aggregate, changes
  end

  # Public: Reverses the effect of #aggregate_changes=
  def reverse_aggregate_changes=(changes)
    self.aggregate_changes = ActiveShepherd::DeepReverseChanges.new(changes).reverse
  end

  # Public: Returns the list of changes to the aggregate that would persist if
  # #save were called on the aggregate root.
  #
  # Examples
  #
  #   @project.aggregate_changes
  #   # => { name: ["Clean House", "Clean My House"], todos: [{ text: ["Take out trash", "Take out the trash" ...
  #
  # Returns all changes in the aggregate
  def aggregate_changes
    ActiveShepherd::Methods::QueryChanges.query_changes aggregate
  end

  # Public: Injects the entire state of the aggregate from a serializable blob.
  #
  # Examples:
  # 
  #   @project.aggregate_state = { name: "Clean House", todos: [{ text: "Take out trash" ...
  #
  # Returns nothing.
  # Raises an AggregateMismatchError if the blob contains references to objects
  #   or attributes that do not exist in this aggregate.
  def aggregate_state=(blob)
    ActiveShepherd::Methods::ApplyState.apply_state aggregate, blob
  end

  # Public: Returns the entire state of the aggregate as a serializable blob.
  # All id values (primary keys) are extracted.
  #
  # Examples
  # 
  #   @project.aggregate_state
  #   # => { name: "Clean House", todos: [{ text: "Take out trash" ...
  #
  # Returns serializable blob.
  def aggregate_state
    ActiveShepherd::Methods::QueryState.query_state aggregate
  end

  # Public: Reloads the entire aggregate by invoking #reload on each of the
  # records in the aggregate.
  def reload_aggregate
    reload
    raise "NYI"
  end

  # Public: Validates a set of changes for the aggregate root.
  #
  #  * Does deep_reverse(deep_reverse(changes)) == changes?
  #  * Assuming the model is currently valid, if the changes were applied,
  #    would the aggregate be valid?
  #  * If I apply the changes, and then apply deep_reverse(changes), does
  #    #aggregate_state change?
  #
  # See ActiveShepherd.deep_reverse
  #
  # Examples:
  # 
  #   @project.valid_aggregate_changes?(@project.aggregate_changes)
  #   # => true
  #
  # Returns true if and only if the supplied changes pass muster.
  def valid_aggregate_changes?(changes, emit_boolean = true)
    validator = ActiveShepherd::ChangesValidator.new(self)
    errors = validator.validate changes
    emit_boolean ? errors.blank? : errors
  ensure
    reload_aggregate
  end

  module ClassMethods
    # Public: Determines whether or not the including class can behave like an
    # aggregate. Designed to be used by tests that want to make sure that any
    # of the models that make up the aggregate never change in a way that would
    # break the functionality of Aggregate::Root.
    #
    # In order for this method to return true, this model and its associated 
    # models are each checked rigorously to ensure they are wired up in a way
    # that meets the requirements of ActiveShepherd. These requirements are:
    #
    #  * All models in the namespace defined by the root itself are visible to
    #    associations that meet this criteria:
    #    * The root model autosaves all associated models in the aggregate.
    #      (:autosave is true on the association)
    #    * The root model validates all associated models in the aggregate.
    #      (:validate is true on the association)
    #    * Associated objects touch the root model when they are updated
    #      (:touch is true on the association)
    #    * When any root model is destroyed, all associated models in the
    #      aggregate boundary are also destroyed, or else their references are
    #      nullified. (:dependent => :destroy/:nullify)
    #    * The entire object constellation within the boundary can be traversed
    #      without accessing the persistence layer, providing they have all been
    #      eager loaded. (:inverse_of is set on the associations)
    #    * If the association references any external aggregate root, then
    #      when that root is deleted, then either the associations reference
    #      must be nullified, or else the associated model itself must be
    #      deleted.
    #  * All models within the aggregate are only referenced inside this
    #    aggregate boundary, with the exception of the root itself.
    #  * Any model in the namespace of the root that has its own sub namespace
    #    under it is recursively checked.
    #
    # Returns true if and only if this model is an aggregate root.
    def behave_like_an_aggregate?(emit_boolean = true)
      errors = ActiveShepherd::ClassValidator.new(self).validate
      emit_boolean ? errors.blank? : errors
    end
  end
end