'use strict';



//region Imports



var util = require('util');

var config = require('./shared/configuration-reader.js');

var Argument = require('./system/argument-check.js');



var CollectionBase = require('./collection-base.js');

var ModelError = require('./shared/model-error.js');

var ExtensionManager = require('./shared/extension-manager.js');

var EventHandlerList = require('./shared/event-handler-list.js');

var DataStore = require('./shared/data-store.js');



var TransferContext = require('./shared/transfer-context.js');



var RuleManager = require('./rules/rule-manager.js');

var BrokenRuleList = require('./rules/broken-rule-list.js');

var AuthorizationAction = require('./rules/authorization-action.js');

var AuthorizationContext = require('./rules/authorization-context.js');

var BrokenRulesResponse = require('./rules/broken-rules-response.js');



var DataPortalAction = require('./shared/data-portal-action.js');

var DataPortalContext = require('./shared/data-portal-context.js');

var DataPortalEvent = require('./shared/data-portal-event.js');

var DataPortalEventArgs = require('./shared/data-portal-event-args.js');

var DataPortalError = require('./shared/data-portal-error.js');



var MODEL_STATE = require('./shared/model-state.js');

var CLASS_NAME = 'EditableRootCollection';

var MODEL_DESC = 'Editable root collection';

var M_FETCH = DataPortalAction.getName(DataPortalAction.fetch);



//endregion



/**

 * Factory method to create definitions of asynchronous editable root collections.

 *

 *    Valid collection item types are:

 *

 *      * EditableChildModel

 *

 * @function bo.EditableRootCollection

 * @param {string} name - The name of the collection.

 * @param {EditableChildModel} itemType - The model type of the collection items.

 * @param {bo.shared.RuleManager} rules - The authorization rules.

 * @param {bo.shared.ExtensionManager} extensions - The customization of the collection.

 * @returns {EditableRootCollection} The constructor of an asynchronous editable root collection.

 *

 * @throws {@link bo.system.ArgumentError Argument error}: The collection name must be a non-empty string.

 * @throws {@link bo.system.ArgumentError Argument error}: The rules must be a RuleManager object.

 * @throws {@link bo.system.ArgumentError Argument error}: The extensions must be a ExtensionManager object.

 * @throws {@link bo.shared.ModelError Model error}: The item type must be an EditableChildModel.

 */

var EditableRootCollectionFactory = function (name, itemType, rules, extensions) {

  var check = Argument.inConstructor(CLASS_NAME);



  name = check(name).forMandatory('name').asString();

  rules = check(rules).forMandatory('rules').asType(RuleManager);

  extensions = check(extensions).forMandatory('extensions').asType(ExtensionManager);



  // Verify the model type of the item type.

  if (itemType.modelType !== 'EditableChildModel')

    throw new ModelError('invalidItem',

        itemType.prototype.name, itemType.modelType,

        CLASS_NAME, 'EditableChildModel');



  // Get data access object.

  var dao = extensions.getDataAccessObject(name);



  /**

   * @classdesc

   *    Represents the definition of an asynchronous editable root collection.

   * @description

   *    Creates a new asynchronous editable root collection instance.

   *

   *    _The name of the model type available as:

   *    __<instance>.constructor.modelType__, returns 'EditableRootCollection'._

   *

   * @name EditableRootCollection

   * @constructor

   * @param {bo.shared.EventHandlerList} [eventHandlers] - The event handlers of the instance.

   *

   * @extends CollectionBase

   *

   * @throws {@link bo.system.ArgumentError Argument error}:

   *    The event handlers must be an EventHandlerList object or null.

   *

   * @fires EditableRootCollection#preCreate

   * @fires EditableRootCollection#postCreate

   * @fires EditableRootCollection#preFetch

   * @fires EditableRootCollection#postFetch

   * @fires EditableRootCollection#preInsert

   * @fires EditableRootCollection#postInsert

   * @fires EditableRootCollection#preUpdate

   * @fires EditableRootCollection#postUpdate

   * @fires EditableRootCollection#preRemove

   * @fires EditableRootCollection#postRemove

   * @fires EditableRootCollection#preSave

   * @fires EditableRootCollection#postSave

   */

  var EditableRootCollection = function (eventHandlers) {

    CollectionBase.call(this);



    eventHandlers = Argument.inConstructor(name)

        .check(eventHandlers).forOptional('eventHandlers').asType(EventHandlerList);



    var self = this;

    var state = null;

    var isDirty = false;

    var items = [];

    var brokenRules = new BrokenRuleList(name);

    var isValidated = false;

    var dataContext = null;

    var connection = null;



    /**

     * The name of the model.

     *

     * @name EditableRootCollection#$modelName

     * @type {string}

     * @readonly

     */

    this.$modelName = name;



    /**

     * The count of the child objects in the collection.

     *

     * @name EditableRootCollection#count

     * @type {number}

     * @readonly

     */

    Object.defineProperty(self, 'count', {

      get: function () {

        return items.length;

      },

      enumerable: false

    });



    // Set up business rules.

    rules.initialize(config.noAccessBehavior);



    // Set up event handlers.

    if (eventHandlers)

      eventHandlers.setup(self);



    //region Mark object state



    /*

     * From:         To:  | pri | cre | cha | mfr | rem

     * -------------------------------------------------

     * NULL               |  +  |  +  |  N  |  N  |  N

     * -------------------------------------------------

     * pristine           |  o  |  -  |  +  |  +  |  -

     * -------------------------------------------------

     * created            |  +  |  o  |  o  | (-) |  +

     * -------------------------------------------------

     * changed            |  +  |  -  |  o  |  +  |  -

     * -------------------------------------------------

     * markedForRemoval   |  -  |  -  |  o  |  o  |  +

     * -------------------------------------------------

     * removed            |  -  |  -  |  -  |  -  |  o

     * -------------------------------------------------

     *

     * Explanation:

     *   +  :  possible transition

     *   -  :  not allowed transition, throws exception

     *   o  :  no change, no action

     *   N  :  impossible start up, throws exception

     */



    function markAsPristine() {

      if (state === MODEL_STATE.markedForRemoval || state === MODEL_STATE.removed)

        illegal(MODEL_STATE.pristine);

      else if (state !== MODEL_STATE.pristine) {

        state = MODEL_STATE.pristine;

        isDirty = false;

      }

    }



    function markAsCreated() {

      if (state === null) {

        state = MODEL_STATE.created;

        isDirty = true;

      }

      else if (state !== MODEL_STATE.created)

        illegal(MODEL_STATE.created);

    }



    function markAsChanged(itself) {

      if (state === MODEL_STATE.pristine) {

        state = MODEL_STATE.changed;

        isDirty = isDirty || itself;

        isValidated = false;

      }

      else if (state === MODEL_STATE.created) {

        isDirty = isDirty || itself;

        isValidated = false;

      }

      else if (state === MODEL_STATE.removed)

        illegal(MODEL_STATE.changed);

    }



    function markForRemoval() {

      if (state === MODEL_STATE.pristine || state === MODEL_STATE.changed) {

        state = MODEL_STATE.markedForRemoval;

        isDirty = true;

        propagateRemoval(); // down to children

      }

      else if (state === MODEL_STATE.created)

        state = MODEL_STATE.removed;

      else if (state !== MODEL_STATE.markedForRemoval)

        illegal(MODEL_STATE.markedForRemoval);

    }



    function markAsRemoved() {

      if (state === MODEL_STATE.created || state === MODEL_STATE.markedForRemoval) {

        state = MODEL_STATE.removed;

        isDirty = false;

      }

      else if (state !== MODEL_STATE.removed)

        illegal(MODEL_STATE.removed);

    }



    function illegal(newState) {

      throw new ModelError('transition',

          (state == null ? 'NULL' : MODEL_STATE.getName(state)),

          MODEL_STATE.getName(newState));

    }



    /**

     * Notes that a child object has changed.

     * <br/>_This method is called by child objects._

     *

     * @function EditableRootCollection#childHasChanged

     * @protected

     */

    this.childHasChanged = function() {

      markAsChanged(false);

    };



    function propagateRemoval() {

      items.forEach(function(child) {

        child.remove();

      });

    }



    //endregion



    //region Show object state



    /**

     * Gets the state of the collection. Valid states are:

     * pristine, created, changed, markedForRemoval and removed.

     *

     * @function EditableRootCollection#getModelState

     * @returns {string} The state of the collection.

     */

    this.getModelState = function () {

      return MODEL_STATE.getName(state);

    };



    /**

     * Indicates whether the business object collection has been created newly

     * and not has been yet saved, i.e. its state is created.

     *

     * @function EditableRootCollection#isNew

     * @returns {boolean} True when the business object collection is new, otherwise false.

     */

    this.isNew = function () {

      return state === MODEL_STATE.created;

    };



    /**

     * Indicates whether the business object collection itself or any of its child objects differs the

     * one that is stored in the repository, i.e. its state is created, changed or markedForRemoval.

     *

     * @function EditableRootCollection#isDirty

     * @returns {boolean} True when the business object collection has been changed, otherwise false.

     */

    this.isDirty = function () {

      return state === MODEL_STATE.created ||

          state === MODEL_STATE.changed ||

          state === MODEL_STATE.markedForRemoval;

    };



    /**

     * Indicates whether the business object collection itself, ignoring its child objects,

     * differs the one that is stored in the repository.

     *

     * @function EditableRootCollection#isSelfDirty

     * @returns {boolean} True when the business object collection itself has been changed, otherwise false.

     */

    this.isSelfDirty = function () {

      return isDirty;

    };



    /**

     * Indicates whether the business object collection will be deleted from the repository,

     * i.e. its state is markedForRemoval.

     *

     * @function EditableRootCollection#isDeleted

     * @returns {boolean} True when the business object collection will be deleted, otherwise false.

     */

    this.isDeleted = function () {

      return state === MODEL_STATE.markedForRemoval;

    };



    /**

     * Indicates whether the business object collection can be saved to the repository,

     * i.e. it has ben changed and is valid, and the user has permission to save it.

     *

     * @function EditableRootCollection#isSavable

     * @returns {boolean} True when the user can save the business object collection, otherwise false.

     */

    this.isSavable = function () {

      var auth;

      if (self.isDeleted)

        auth = canDo(AuthorizationAction.removeObject);

      else if (self.isNew)

        auth = canDo(AuthorizationAction.createObject);

      else

        auth = canDo(AuthorizationAction.updateObject);

      return auth && self.isDirty && self.isValid();

    };



    //endregion



    //region Transfer object methods



    function getTransferContext (authorize) {

      return new TransferContext(null, null, null);

    }



    function baseToCto() {

      var cto = [];

      items.forEach(function (item) {

        cto.push(item.toCto());

      });

      return cto;

    }



    /**

     * Transforms the business object collection to a plain object array to send to the client.

     *

     * @function EditableRootCollection#toCto

     * @returns {object} The client transfer object.

     */

    this.toCto = function () {

      if (extensions.toCto)

        return extensions.toCto.call(self, getTransferContext());

      else

        return baseToCto();

    };



    function baseFromCto(data) {

      // Nothing to do.

    }



    /**

     * Rebuilds the business object collection from a plain object array sent by the client.

     *

     * @function EditableRootCollection#fromCto

     * @param {object} cto - The client transfer object.

     * @param {external.cbFromCto} callback - Returns the eventual error.

     */

    this.fromCto = function (cto, callback) {

      if (extensions.fromCto)

        extensions.fromCto.call(self, getTransferContext(true), cto);

      else

        baseFromCto(cto);



      // Build children.

      if (!(cto instanceof Array))

        callback(null);



      var dataNew = cto.filter(function () { return true; });

      var itemsLive = [];

      var itemsLate = [];

      var count = 0;

      var error = null;

      var index;



      function finish() {

        if (++count == dataNew.length) {

          return callback(error);

        }

      }

      function handleOldNew() {

        count = 0;



        // Remove non existing items.

        for (index = 0; index < itemsLate.length; index++) {

          items[itemsLate[index]].remove();

        }

        // Insert non existing data.

        if (dataNew.length) {

          dataNew.forEach(function (cto) {

            itemType.create(self, eventHandlers, function (err, item) {

              if (err) {

                error = error || err;

                finish();

              } else {

                item.fromCto(cto, function (err) {

                  if (err)

                    error = error || err;

                  else

                    items.push(item);

                  finish();

                });

              }

            });

          });

        } else

          return callback(null);

      }



      // Discover changed items.

      for (index = 0; index < items.length; index++) {

        var dataFound = false;

        var i = 0;

        for (; i < dataNew.length; i++) {

          if (items[index].keyEquals(cto[i])) {

            itemsLive.push({ item: index, cto: i });

            dataFound = true;

            break;

          }

        }

        if (dataFound)

          dataNew.splice(i, 1);

        else

          itemsLate.push(index);

      }

      // Update existing items.

      if (itemsLive.length)

        for (index = 0; index < itemsLive.length; index++) {

          var ix = itemsLive[index];

          items[ix.item].fromCto(cto[ix.cto], function (err) {

            if (err)

              error = error || err;

            if (++count == itemsLive.length)

              handleOldNew();

          });

        }

      else

        handleOldNew();

    };



    //endregion



    //region Permissions



    function getAuthorizationContext(action, targetName) {

      return new AuthorizationContext(action, targetName || '', brokenRules);

    }



    function canDo (action) {

      return rules.hasPermission(

          getAuthorizationContext(action)

      );

    }



    function canExecute (methodName) {

      return rules.hasPermission(

          getAuthorizationContext(AuthorizationAction.executeMethod, methodName)

      );

    }



    //endregion



    //region Child methods



    function fetchChildren (dto, callback) {

      if (dto instanceof Array && dto.length) {

        var count = 0;

        var error = null;



        dto.forEach(function (data) {

          itemType.load(self, data, eventHandlers, function (err, item) {

            if (err)

              error = error || err;

            else

              items.push(item);

            // Check if all items are done.

            if (++count === dto.length)

              callback(error);

          });

        });

      } else

        callback(null);

    }



    function insertChildren(connection, callback) {

      saveChildren(connection, callback);

    }



    function updateChildren(connection, callback) {

      saveChildren(connection, callback);

    }



    function removeChildren(connection, callback) {

      saveChildren(connection, callback);

    }



    function saveChildren(connection, callback) {

      if (items.length) {

        var count = 0;

        var error = null;



        items.forEach(function (item) {

          item.save(connection, function (err) {

            error = error || err;

            // Check if all items are done.

            if (++count === items.length) {

              callback(error);

            }

          });

        });

      } else

        callback(null);

    }



    function childrenAreValid () {

      return items.every(function (item) {

        return item.isValid();

      });

    }



    function checkChildRules () {

      items.forEach(function (item) {

        item.checkRules();

      });

    }



    //endregion



    //region Data portal methods



    //region Helper



    function getDataContext (connection) {

      if (!dataContext)

        dataContext = new DataPortalContext(dao);

      return dataContext.setState(connection, false);

    }



    function raiseEvent (event, methodName, error) {

      self.emit(

          DataPortalEvent.getName(event),

          new DataPortalEventArgs(event, name, null, methodName, error)

      );

    }



    function raiseSave (event, action, error) {

      self.emit(

          DataPortalEvent.getName(event),

          new DataPortalEventArgs(event, name, action, null, error)

      );

    }



    function wrapError (error) {

      return new DataPortalError(MODEL_DESC, name, DataPortalAction.fetch, error);

    }



    function runStatements (main, action, callback) {

      // Open connection.

      config.connectionManager.openConnection(

          extensions.dataSource, function (errOpen, connection) {

            if (errOpen)

              callback(wrapError(action, errOpen));

            else

              main(connection, function (err, result) {

                // Close connection.

                config.connectionManager.closeConnection(

                    extensions.dataSource, connection, function (errClose, connClosed) {

                      connection = connClosed;

                      if (err)

                        callback(wrapError(action, err));

                      else if (errClose)

                        callback(wrapError(action, errClose));

                      else

                        callback(null, result);

                    });

              });

          });

    }



    function runTransaction (main, action, callback) {

      // Start transaction.

      config.connectionManager.beginTransaction(

          extensions.dataSource, function (errBegin, connection) {

            if (errBegin)

              callback(wrapError(action, errBegin));

            else

              main(connection, function (err, result) {

                if (err)

                // Undo transaction.

                  config.connectionManager.rollbackTransaction(

                      extensions.dataSource, connection, function (errRollback, connClosed) {

                        connection = connClosed;

                        callback(wrapError(action, err));

                      });

                else

                // Finish transaction.

                  config.connectionManager.commitTransaction(

                      extensions.dataSource, connection, function (errCommit, connClosed) {

                        connection = connClosed;

                        if (errCommit)

                          callback(wrapError(action, errCommit));

                        else

                          callback(null, result);

                      });

              });

          });

    }



    //endregion



    //region Create



    function data_create (callback) {

      var hasConnection = false;

      // Helper callback for post-creation actions.

      function finish (cb) {

        markAsCreated();

        // Launch finish event.

        /**

         * The event arises after the business object collection has been initialized in the repository.

         * @event EditableRootCollection#postCreate

         * @param {bo.shared.DataPortalEventArgs} eventArgs - Data portal event arguments.

         * @param {EditableRootCollection} newObject - The instance of the collection after the data portal action.

         */

        raiseEvent(DataPortalEvent.postCreate);

        cb(null, self);

      }

      // Main activity.

      function main (connection, cb) {

        hasConnection = connection !== null;

        // Launch start event.

        /**

         * The event arises before the business object collection will be initialized in the repository.

         * @event EditableRootCollection#preCreate

         * @param {bo.shared.DataPortalEventArgs} eventArgs - Data portal event arguments.

         * @param {EditableRootCollection} oldObject - The instance of the collection before the data portal action.

         */

        raiseEvent(DataPortalEvent.preCreate);

        // Execute creation - nothing to do.

        finish(cb);

      }

      runStatements(main, DataPortalAction.create, callback);

    }



    //endregion



    //region Fetch



    function data_fetch (filter, method, callback) {

      var hasConnection = false;

      // Helper function for post-fetch actions.

      function finish (dto, cb) {

        // Load children.

        fetchChildren(dto, function (err) {

          if (err)

            cb(err);

          else {

            markAsPristine();

            // Launch finish event.

            /**

             * The event arises after the collection instance has been retrieved from the repository.

             * @event EditableRootCollection#postFetch

             * @param {bo.shared.DataPortalEventArgs} eventArgs - Data portal event arguments.

             * @param {EditableRootCollection} newObject - The collection instance after the data portal action.

             */

            raiseEvent(DataPortalEvent.postFetch, method);

            cb(null, self);

          }

        });

      }

      // Helper callback for failure.

      function failed (err, cb) {

        if (hasConnection) {

          // Launch finish event.

          var dpError = wrapError(DataPortalAction.fetch, err);

          raiseEvent(DataPortalEvent.postFetch, method, dpError);

        }

        cb(err);

      }

      // Main activity.

      function main (connection, cb) {

        hasConnection = connection !== null;

        // Launch start event.

        /**

         * The event arises before the collection instance will be retrieved from the repository.

         * @event EditableRootCollection#preFetch

         * @param {bo.shared.DataPortalEventArgs} eventArgs - Data portal event arguments.

         * @param {EditableRootCollection} oldObject - The collection instance before the data portal action.

         */

        raiseEvent(DataPortalEvent.preFetch, method);

        // Execute fetch.

        if (extensions.dataFetch) {

          // *** Custom fetch.

          extensions.dataFetch.call(self, getDataContext(connection), filter, method, function (err, dto) {

            if (err)

              failed(err, cb);

            else

              finish(dto, cb);

          });

        } else {

          // *** Standard fetch.

          // Root element fetches data from repository.

          dao.$runMethod(method, connection, filter, function (err, dto) {

            if (err)

              failed(err, cb);

            else {

              finish(dto, cb);

            }

          });

        }

      }

      // Check permissions.

      if (method === M_FETCH ? canDo(AuthorizationAction.fetchObject) : canExecute(method))

        runStatements(main, DataPortalAction.fetch, callback);

      else

        callback(null, self);

    }



    //endregion



    //region Insert



    function data_insert (callback) {

      var hasConnection = false;

      // Helper function for post-insert actions.

      function finish (connection, cb) {

        // Insert children as well.

        insertChildren(connection, function (err) {

          if (err)

            failed(err, cb);

          else {

            markAsPristine();

            // Launch finish event.

            /**

             * The event arises after the business object collection has been created in the repository.

             * @event EditableRootCollection#postInsert

             * @param {bo.shared.DataPortalEventArgs} eventArgs - Data portal event arguments.

             * @param {EditableRootCollection} newObject - The instance of the collection after the data portal action.

             */

            raiseEvent(DataPortalEvent.postInsert);

            raiseSave(DataPortalEvent.postSave, DataPortalAction.insert);

            cb(null, self);

          }

        });

      }

      // Helper callback for failure.

      function failed (err, cb) {

        if (hasConnection) {

          // Launch finish event.

          var dpError = wrapError(DataPortalAction.insert, err);

          raiseEvent(DataPortalEvent.postInsert, null, dpError);

          raiseSave(DataPortalEvent.postSave, DataPortalAction.insert, dpError);

        }

        cb(err);

      }

      // Main activity.

      function main (connection, cb) {

        hasConnection = connection !== null;

        // Launch start event.

        raiseSave(DataPortalEvent.preSave, DataPortalAction.insert);

        /**

         * The event arises before the business object collection will be created in the repository.

         * @event EditableRootCollection#preInsert

         * @param {bo.shared.DataPortalEventArgs} eventArgs - Data portal event arguments.

         * @param {EditableRootCollection} oldObject - The instance of the collection before the data portal action.

         */

        raiseEvent(DataPortalEvent.preInsert);

        // Execute insert - nothing to do.

        finish(connection, cb);

      }

      // Check permissions.

      if (canDo(AuthorizationAction.createObject))

        runTransaction(main, DataPortalAction.insert, callback);

      else

        callback(null, self);

    }



    //endregion



    //region Update



    function data_update (callback) {

      var hasConnection = false;

      // Helper function for post-update actions.

      function finish (connection, cb) {

        // Update children as well.

        updateChildren(connection, function (err) {

          if (err)

            failed(err, cb);

          else {

            markAsPristine();

            // Launch finish event.

            /**

             * The event arises after the business object collection has been updated in the repository.

             * @event EditableRootCollection#postUpdate

             * @param {bo.shared.DataPortalEventArgs} eventArgs - Data portal event arguments.

             * @param {EditableRootCollection} newObject - The instance of the collection after the data portal action.

             */

            raiseEvent(DataPortalEvent.postUpdate);

            raiseSave(DataPortalEvent.postSave, DataPortalAction.update);

            cb(null, self);

          }

        });

      }

      // Helper callback for failure.

      function failed (err, cb) {

        if (hasConnection) {

          // Launch finish event.

          var dpError = wrapError(DataPortalAction.update, err);

          raiseEvent(DataPortalEvent.postUpdate, null, dpError);

          raiseSave(DataPortalEvent.postSave, DataPortalAction.update, dpError);

        }

        cb(err);

      }

      // Main activity.

      function main (connection, cb) {

        hasConnection = connection !== null;

        // Launch start event.

        raiseSave(DataPortalEvent.preSave, DataPortalAction.update);

        /**

         * The event arises before the business object collection will be updated in the repository.

         * @event EditableRootCollection#preUpdate

         * @param {bo.shared.DataPortalEventArgs} eventArgs - Data portal event arguments.

         * @param {EditableRootCollection} oldObject - The instance of the collection before the data portal action.

         */

        raiseEvent(DataPortalEvent.preUpdate);

        // Execute update - nothing to do.

        finish(connection, cb);

      }

      // Check permissions.

      if (canDo(AuthorizationAction.updateObject))

        runTransaction(main, DataPortalAction.update, callback);

      else

        callback(null, self);

    }



    //endregion



    //region Remove



    function data_remove (callback) {

      var hasConnection = false;

      // Helper callback for post-removal actions.

      function finish (cb) {

        markAsRemoved();

        // Launch finish event.

        /**

         * The event arises after the business object collection has been removed from the repository.

         * @event EditableRootCollection#postRemove

         * @param {bo.shared.DataPortalEventArgs} eventArgs - Data portal event arguments.

         * @param {EditableRootCollection} newObject - The instance of the collection after the data portal action.

         */

        raiseEvent(DataPortalEvent.postRemove);

        raiseSave(DataPortalEvent.postSave, DataPortalAction.remove);

        cb(null, null);

      }

      // Helper callback for failure.

      function failed (err, cb) {

        if (hasConnection) {

          // Launch finish event.

          var dpError = wrapError(DataPortalAction.remove, err);

          raiseEvent(DataPortalEvent.postRemove, null, dpError);

          raiseSave(DataPortalEvent.postSave, DataPortalAction.remove, dpError);

        }

        cb(err);

      }

      // Main activity.

      function main (connection, cb) {

        hasConnection = connection !== null;

        // Launch start event.

        raiseSave(DataPortalEvent.preSave, DataPortalAction.remove);

        /**

         * The event arises before the business object collection will be removed from the repository.

         * @event EditableRootCollection#preRemove

         * @param {bo.shared.DataPortalEventArgs} eventArgs - Data portal event arguments.

         * @param {EditableRootCollection} oldObject - The instance of the collection before the data portal action.

         */

        raiseEvent(DataPortalEvent.preRemove);

        // Remove children first.

        removeChildren(connection, function (err) {

          if (err)

            failed(err, cb);

          else {

            // Execute removal - nothing to do.

            finish(cb);

          }

        });

      }

      // Check permissions.

      if (canDo(AuthorizationAction.removeObject))

        runTransaction(main, DataPortalAction.remove, callback);

      else

        callback(null, null);

    }



    //endregion



    //endregion



    //region Actions



    /**

     * Initializes a newly created business object collection.

     * <br/>_This method is called by a factory method with the same name._

     *

     * @function EditableRootCollection#create

     * @protected

     * @param {external.cbDataPortal} callback - Returns a new editable business object collection.

     *

     * @throws {@link bo.system.ArgumentError Argument error}:

     *      The callback must be a function.

     * @throws {@link bo.rules.AuthorizationError Authorization error}:

     *      The user has no permission to execute the action.

     * @throws {@link bo.shared.DataPortalError Data portal error}:

     *      Creating the business object collection has failed.

     */

    this.create = function(callback) {



      callback = Argument.inMethod(name, 'create')

          .check(callback).forMandatory('callback').asFunction();



      data_create(callback);

    };



    /**

     * Creates a new item and adds it to the collection at the specified index.

     *

     * @function EditableRootCollection#create

     * @param {number} index - The index of the new item.

     * @param {external.cbDataPortal} callback - Returns the newly created business object

     *      of the collection.

     */

    this.createItem = function (index, callback) {

      if (!callback) {

        callback = index;

        index = items.length;

      }



      callback = Argument.inMethod(name, 'createItem')

          .check(callback).forMandatory('callback').asFunction();



      itemType.create(self, eventHandlers, function (err, item) {

        if (err)

          callback(err);

        else {

          var ix = parseInt(index, 10);

          ix = isNaN(ix) ? items.length : ix;

          items.splice(ix, 0, item);

          callback(null, item);

        }

      });

    };



    /**

     * Initializes a business object collection to be retrieved from the repository.

     * <br/>_This method is called by a factory method with the same name._

     *

     * @function EditableRootCollection#fetch

     * @protected

     * @param {*} [filter] - The filter criteria.

     * @param {string} [method] - An alternative fetch method of the data access object.

     * @param {external.cbDataPortal} callback - Returns the required editable business object collection.

     *

     * @throws {@link bo.system.ArgumentError Argument error}:

     *      The method must be a string or null.

     * @throws {@link bo.system.ArgumentError Argument error}:

     *      The callback must be a function.

     * @throws {@link bo.rules.AuthorizationError Authorization error}:

     *      The user has no permission to execute the action.

     * @throws {@link bo.shared.DataPortalError Data portal error}:

     *      Fetching the business object collection has failed.

     */

    this.fetch = function(filter, method, callback) {

      var check = Argument.inMethod(name, 'fetch');



      method = check(method).forOptional('method').asString();

      callback = check(callback).forMandatory('callback').asFunction();



      data_fetch(filter, method || M_FETCH, callback);

    };



    /**

     * Saves the changes of the business object collection to the repository.

     *

     * @function EditableRootCollection#save

     * @param {external.cbDataPortal} callback - Returns the business object

     *      collection with the new state after the save.

     *

     * @throws {@link bo.system.ArgumentError Argument error}:

     *      The callback must be a function.

     * @throws {@link bo.rules.AuthorizationError Authorization error}:

     *      The user has no permission to execute the action.

     * @throws {@link bo.shared.DataPortalError Data portal error}:

     *      Inserting the business object collection has failed.

     * @throws {@link bo.shared.DataPortalError Data portal error}:

     *      Updating the business object collection has failed.

     * @throws {@link bo.shared.DataPortalError Data portal error}:

     *      Deleting the business object collection has failed.

     */

    this.save = function(callback) {



      callback = Argument.inMethod(name, 'save')

          .check(callback).forMandatory('callback').asFunction();



      function clearRemovedItems() {

        items = items.filter(function (item) {

          return item.getModelState() !== MODEL_STATE.getName(MODEL_STATE.removed);

        });

      }

      if (this.isValid()) {

        /**

         * The event arises before the business object collection will be saved in the repository.

         * The event is followed by a preInsert, preUpdate or preRemove event depending on the

         * state of the business object collection.

         * @event EditableRootCollection#preSave

         * @param {bo.shared.DataPortalEventArgs} eventArgs - Data portal event arguments.

         * @param {EditableRootCollection} oldObject - The instance of the collection before the data portal action.

         */

        switch (state) {

          case MODEL_STATE.created:

            data_insert(callback);

            break;

          case MODEL_STATE.changed:

            data_update(function (err, updated) {

              if (err)

                callback(err);

              else {

                clearRemovedItems();

                callback(null, self);

              }

            });

            break;

          case MODEL_STATE.markedForRemoval:

            data_remove(function (err, removed) {

              if (err)

                callback(err);

              else {

                clearRemovedItems();

                callback(null, null);

              }

            });

            break;

          default:

            callback(null, this);

        }

        /**

         * The event arises after the business object collection has been saved in the repository.

         * The event is preceded by a postInsert, postUpdate or postRemove event depending on the

         * state of the business object collection.

         * @event EditableRootCollection#postSave

         * @param {bo.shared.DataPortalEventArgs} eventArgs - Data portal event arguments.

         * @param {EditableRootCollection} newObject - The instance of the collection after the data portal action.

         */

      }

    };



    /**

     * Marks the business object collection to be deleted from the repository on next save.

     *

     * @function EditableRootCollection#remove

     */

    this.remove = function() {

      markForRemoval();

    };



    //endregion



    //region Validation



    /**

     * Indicates whether all validation rules of all business objects of the

     * collection succeeds. A valid business object collection may have

     * broken rules with severity of success, information and warning.

     *

     * @function EditableRootCollection#isValid

     * @returns {boolean} True when the business object collection is valid, otherwise false.

     */

    this.isValid = function () {

      if (!isValidated)

        this.checkRules();



      return brokenRules.isValid() && childrenAreValid();

    };



    /**

     * Executes all the validation rules of the business object, including the ones

     * of its child objects.

     *

     * @function EditableRootCollection#checkRules

     */

    this.checkRules = function () {

      brokenRules.clear();



      checkChildRules();



      isValidated = true;

    };



    /**

     * Gets the broken rules of the business object.

     *

     * @function EditableRootCollection#getBrokenRules

     * @param {string} [namespace] - The namespace of the message keys when messages are localizable.

     * @returns {bo.rules.BrokenRulesOutput} The broken rules of the business object.

     */

    this.getBrokenRules = function (namespace) {

      var bro = brokenRules.output(namespace);



      items.forEach(function (item) {

        var childBrokenRules = item.getBrokenRules(namespace);

        if (childBrokenRules)

          bro.addChild(name, childBrokenRules);

      });



      return bro.$length ? bro : null;

    };



    /**

     * Gets the response to send to the client in case of broken rules.

     *

     * @function EditableRootCollection#getResponse

     * @param {string} [message] - Human-readable description of the reason of the failure.

     * @param {string} [namespace] - The namespace of the message keys when messages are localizable.

     * @returns {bo.rules.BrokenRulesResponse} The broken rules response to send to the client.

     */

    this.getResponse = function (message, namespace) {

      var output = this.getBrokenRules(namespace);

      return output ? new BrokenRulesResponse(output, message) : null;

    };



    //endregion



    //region Public array methods



    /**

     * Gets a collection item at a specific position.

     *

     * @function EditableRootCollection#at

     * @param {number} index - The index of the required item in the collection.

     * @returns {EditableChildModel} The required collection item.

     */

    this.at = function (index) {

      return items[index];

    };



    /**

     * Executes a provided function once per collection item.

     *

     * @function EditableRootCollection#forEach

     * @param {external.cbCollectionItem} callback - Function that produces an item of the new collection.

     */

    this.forEach = function (callback) {

      items.forEach(callback);

    };



    /**

     * Tests whether all items in the collection pass the test implemented by the provided function.

     *

     * @function EditableRootCollection#every

     * @param {external.cbCollectionItem} callback - Function to test for each collection item.

     * @returns {boolean} True when callback returns truthy value for each item, otherwise false.

     */

    this.every = function (callback) {

      return items.every(callback);

    };



    /**

     * Tests whether some item in the collection pass the test implemented by the provided function.

     *

     * @function EditableRootCollection#some

     * @param {external.cbCollectionItem} callback - Function to test for each collection item.

     * @returns {boolean} True when callback returns truthy value for some item, otherwise false.

     */

    this.some = function (callback) {

      return items.some(callback);

    };



    /**

     * Creates a new array with all collection items that pass the test

     * implemented by the provided function.

     *

     * @function EditableRootCollection#filter

     * @param {external.cbCollectionItem} callback - Function to test for each collection item.

     * @returns {Array.<EditableChildModel>} The new array of collection items.

     */

    this.filter = function (callback) {

      return items.filter(callback);

    };



    /**

     * Creates a new array with the results of calling a provided function

     * on every item in this collection.

     *

     * @function EditableRootCollection#map

     * @param {external.cbCollectionItem} callback - Function to test for each collection item.

     * @returns {Array.<*>} The new array of callback results.

     */

    this.map = function (callback) {

      return items.map(callback);

    };



    /**

     * Sorts the items of the collection in place and returns the collection.

     *

     * @function EditableRootCollection#sort

     * @param {external.cbCompare} [fnCompare] - Function that defines the sort order.

     *      If omitted, the collection is sorted according to each character's Unicode

     *      code point value, according to the string conversion of each item.

     * @returns {Array.<EditableChildModel>} The sorted collection.

     */

    this.sort = function (fnCompare) {

      return items.sort(fnCompare);

    };



    //endregion



    // Immutable object.

    Object.freeze(this);

  };

  util.inherits(EditableRootCollection, CollectionBase);



  /**

   * The name of the model type.

   *

   * @property {string} EditableRootCollection.constructor.modelType

   * @default EditableRootCollection

   * @readonly

   */

  Object.defineProperty(EditableRootCollection, 'modelType', {

    get: function () { return CLASS_NAME; }

  });



  //region Factory methods



  /**

   * Creates a new editable business object collection.

   *

   * @function EditableRootCollection.create

   * @param {bo.shared.EventHandlerList} [eventHandlers] - The event handlers of the instance.

   * @param {external.cbDataPortal} callback - Returns a new editable business object collection.

   *

   * @throws {@link bo.system.ArgumentError Argument error}:

   *      The event handlers must be an EventHandlerList object or null.

   * @throws {@link bo.rules.AuthorizationError Authorization error}:

   *      The user has no permission to execute the action.

   * @throws {@link bo.shared.DataPortalError Data portal error}:

   *      Creating the business object collection has failed.

   */

  EditableRootCollection.create = function(eventHandlers, callback) {

    var instance = new EditableRootCollection(eventHandlers);

    instance.create(function (err) {

      if (err)

        callback(err);

      else

        callback(null, instance);

    });

  };



  /**

   * Retrieves an editable business object collection from the repository.

   *

   * @function EditableRootCollection.fetch

   * @param {*} [filter] - The filter criteria.

   * @param {string} [method] - An alternative fetch method of the data access object.

   * @param {bo.shared.EventHandlerList} [eventHandlers] - The event handlers of the instance.

   * @param {external.cbDataPortal} callback - Returns the required editable business object collection.

   *

   * @throws {@link bo.system.ArgumentError Argument error}:

   *      The method must be a string or null.

   * @throws {@link bo.system.ArgumentError Argument error}:

   *      The event handlers must be an EventHandlerList object or null.

   * @throws {@link bo.rules.AuthorizationError Authorization error}:

   *      The user has no permission to execute the action.

   * @throws {@link bo.shared.DataPortalError Data portal error}:

   *      Fetching the business object collection has failed.

   */

  EditableRootCollection.fetch = function(filter, method, eventHandlers, callback) {

    var instance = new EditableRootCollection(eventHandlers);

    instance.fetch(filter, method, function (err) {

      if (err)

        callback(err);

      else

        callback(null, instance);

    });

  };



  //endregion



  return EditableRootCollection;

};



module.exports = EditableRootCollectionFactory;