# Source: data/service/data-trigger.js

    var Montage = require("core/core").Montage,
        DataObjectDescriptor = require("data/model/data-object-descriptor").DataObjectDescriptor,
        ObjectDescriptor = require("data/model/object-descriptor").ObjectDescriptor,
        WeakMap = require("collections/weak-map"),
        DataTrigger;

    /**
     * Intercepts all calls to get and set an object's property and triggers any
     * Montage Data action warranted by these calls.
     *
     * DataTrigger is a JavaScript Objects subclass rather than a Montage subclass
     * so data triggers can be as lightweight as possible: They need to be
     * lightweight because many will be created (one for each relationship or
     * lazily loaded property of each model class) and there's no benefit for them
     * to be derived from the Montage prototype because they don't use any of the
     * Montage class functionality.
     *
     * @private
     * @class
     * @extends Object
     */
    DataTrigger = exports.DataTrigger = function () {};

    exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototype */ {

        /**
         * The constructor function for all trigger instances.
         *
         * @type {function}
         */
        constructor: {
            configurable: true,
            writable: true,
            value: exports.DataTrigger
        },

        /**
         * The service used by this trigger to perform Montage Data actions.
         *
         * Typically a number of triggers use the same service so this property
         * is defined in a single DataTrigger instance for each service and all
         * triggers that share that service are then derived from that instance.
         * This avoids the need for each trigger to have a reference to the service
         * it uses and saves memory. See
         * [_getTriggerPrototype()]{@link DataTrigger._getTriggerPrototype} for
         * the implementation of this behavior.
         *
         * @private
         * @type {Service}
         */
        _service: {
            configurable: true,
            writable: true,
            value: undefined
        },

        /**
         * The prototype of objects whose property is managed by this trigger.
         *
         * @private
         * @type {Object}
         */
        _objectPrototype: {
            configurable: true,
            writable: true,
            value: undefined
        },

        /**
         * The name of the property managed by this trigger.
         *
         * @private
         * @type {string}
         */
        _propertyName: {
            configurable: true,
            writable: true,
            value: undefined
        },

        /**
         * The name of the private property corresponding to the public property
         * managed by this trigger.
         *
         * The private property name is the
         * [_propertyName]{@link DataTrigger#_propertyName} value prefixed with an
         * underscore. To minimize the time and memory used by a trigger's call
         * intercepts this private property name is generated lazilly the first time
         * it is needed and then cached.
         *
         * @private
         * @type {string}
         */
        _privatePropertyName: {
            configurable: true,
            get: function () {
                if (!this.__privatePropertyName && this._propertyName) {
                    this.__privatePropertyName = "_" + this._propertyName;
                }
                return this.__privatePropertyName;
            }
        },

        /**
         * Whether this trigger is for a global property or not.
         *
         * When a trigger is global and the value of the trigger's property is
         * obtained or set for one object managed by the trigger then that
         * property's value is assumed to have also been obtained or set for all
         * objects managed by the trigger.
         *
         * Setting this value clears the
         * [_valueStatus]{@link DataTrigger#_valueStatus} and should only be done
         * before a trigger is used.
         *
         * @private
         * @type {string}
         */
        _isGlobal: {
            configurable: true,
            get: function () {
                // To save memory a separate _isGlobal boolean is not maintained and
                // the _isGlobal value is derived from the _valueStatus type.
                return !(this._valueStatus instanceof WeakMap);
            },
            set: function (global) {
                global = global ? true : false;
                if (global !== this._isGlobal) {
                    this._valueStatus = global ? undefined : new WeakMap();
                }
            }
        },

        /**
         * For global triggers, holds the status of this trigger's property value.
         * For other triggers, holds a map from objects whose property is managed
         * by this trigger to the status of that property's value for each of those
         * objects.
         *
         * See [_getValueStatus()]{@link DataTrigger#_getValueStatus} for the
         * possible status values.
         *
         * @private
         * @type {Object|WeakMap}
         */
        _valueStatus: {
            configurable: true,
            writable: true,
            value: undefined
        },

        /**
         * Gets the status of a property value managed by this trigger:
         *
         * - The status will be `undefined` when the value has not yet been
         *   requested or set.
         *
         * - The status will be `null` when the value was requested and has already
         *   been obtained or when it has been set.
         *
         * - When the value has been requested but is still in the process of being
         *   obtained, the status will be an object with a "promise" property set to
         *   a promise that will be resolved when the value is obtained and a
         *   "resolve" property set to a function that will resolve that promise.
         *
         * @private
         * @method
         * @argument {Object} object
         * @returns {Object}
         */
        _getValueStatus: {
            configurable: true,
            value: function (object) {
                return this._isGlobal ? this._valueStatus : this._valueStatus.get(object);
            }
        },

        /**
         * Sets the status of a property value managed by this trigger.
         *
         * See [_getValueStatus()]{@link DataTrigger#_getValueStatus} for the
         * possible status values.
         *
         * @private
         * @method
         * @argument {Object} object
         * @argument {Object} status
         */
        _setValueStatus: {
            configurable: true,
            value: function (object, status) {
                if (this._isGlobal) {
                    this._valueStatus = status;
                } else if (status !== undefined) {
                    this._valueStatus.set(object, status);
                } else {
                    this._valueStatus.delete(object);
                }
            }
        },

        /**
         * @method
         * @argument {Object} object
         * @returns {Object}
         *
         * #Performance #ToDo: Looks like the same walk-up logic
         * is going to be done many times for individual instances,
         * we should improve that.
         */
        _getValue: {
            configurable: true,
            writable: true,
            value: function (object) {
                var prototype, descriptor, getter;
                // Start an asynchronous fetch of the property's value if necessary.
                this.getObjectProperty(object);
                // Search the prototype chain for a getter for this property,
                // starting just after the prototype that called this method.
                prototype = Object.getPrototypeOf(this._objectPrototype);
                while (prototype) {
                    descriptor = Object.getOwnPropertyDescriptor(prototype, this._propertyName);
                    getter = descriptor && descriptor.get;
                    prototype = !getter && Object.getPrototypeOf(prototype);
                }
                // Return the property's current value.
                return getter ? getter.call(object) : object[this._privatePropertyName];
            }
        },

        /**
         * Note that if a trigger's property value is set after that values is
         * requested but before it is obtained from the trigger's service the
         * property's value will only temporarily be set to the specified value:
         * When the service finishes obtaining the value the property's value will
         * be reset to that obtained value.
         *
         * @method
         * @argument {Object} object
         * @argument {} value
         */
        _setValue: {
            configurable: true,
            writable: true,
            value: function (object, value) {
                var status, prototype, descriptor, getter, setter, writable;
                // Get the value's current status and update that status to indicate
                // the value has been obtained. This way if the setter called below
                // requests the property's value it will get the value the property
                // had before it was set, and it will get that value immediately.
                status = this._getValueStatus(object);
                this._setValueStatus(object, null);
                // Search the prototype chain for a setter for this trigger's
                // property, starting just after the trigger prototype that caused
                // this method to be called.
                prototype = Object.getPrototypeOf(this._objectPrototype);
                while (prototype) {
                    descriptor = Object.getOwnPropertyDescriptor(prototype, this._propertyName);
                    getter = descriptor && descriptor.get;
                    setter = getter && descriptor.set;
                    writable = !descriptor || setter || descriptor.writable;
                    prototype = writable && !setter && Object.getPrototypeOf(prototype);
                }
                // Set this trigger's property to the desired value, but only if
                // that property is writable.
                if (setter) {
                    setter.call(object, value);
                } else if (writable) {
                    object[this._privatePropertyName] = value;
                }
                // Resolve any pending promise for this trigger's property value.
                if (status) {
                    status.resolve(null);
                }
            }
        },

        /**
         * @todo Rename and document API and implementation.
         *
         * @method
         */
        decacheObjectProperty: {
            value: function (object) {
                this._setValueStatus(object, undefined);
            }
        },
        /**
         * Request a fetch of the value of this trigger's property for the
         * specified object but only if that data isn't already in the process
         * of being obtained and only if it wasn't previously obtained or
         * set. To unconditionally request a fetch of this property data use
         * [updateObjectProperty()]{@link DataTrigger#updateObjectProperty}.
         *
         * @method
         * @argument {Object} object
         * @returns {external:Promise}
         */
        getObjectProperty: {
            value: function (object) {
                var status = this._getValueStatus(object);
                return  status ?             status.promise :
                        status === null ?   this._service.nullPromise :
                                            this.updateObjectProperty(object);
            }
        },

        /**
         * If the value of this trigger's property for the specified object isn't in
         * the process of being obtained, request the most up to date value of that
         * data from this trigger's service.
         *
         * @method
         * @argument {Object} object
         * @returns {external:Promise}
         */
        updateObjectProperty: {
            value: function (object) {
                var self = this,
                    status = this._getValueStatus(object) || {};
                if (!status.promise) {
                    this._setValueStatus(object, status);
                    status.promise = new Promise(function (resolve, reject) {
                        status.resolve = resolve;
                        status.reject = reject;
                        self._fetchObjectProperty(object);
                    });
                }
                // Return the existing or just created promise for this data.
                return status.promise;
            }
        },

        /**
         * @private
         * @method
         * @argument {Object} object
         * @returns {external:Promise}
         */
        _fetchObjectProperty: {
            value: function (object) {
                var self = this;
                this._service.fetchObjectProperty(object, this._propertyName).then(function () {
                    return self._fulfillObjectPropertyFetch(object);
                }).catch(function (error) {
                    console.error(error);
                    return self._fulfillObjectPropertyFetch(object, error);
                });
            }
        },

        _fulfillObjectPropertyFetch: {
            value: function (object, error) {
                var status = this._getValueStatus(object);
                this._setValueStatus(object, null);
                if (status && !error) {
                    status.resolve(null);
                } else if (status && error) {
                    console.error(error);
                    status.reject(error);
                }
                return null;
            }
        }

    });

    Object.defineProperties(exports.DataTrigger, /** @lends DataTrigger */ {

        /**
         * @method
         * @argument {DataService} service
         * @argument {Object} prototype
         * @argument {Set} property names to exclude from triggers.
         * @returns {Object.<string, DataTrigger>}
         */
        addTriggers: {
            value: function (service, type, prototype, requisitePropertyNames) {
                // This function was split into two to provide backwards compatibility
                // to existing Montage data projects.  Future montage data projects
                // should base their object descriptors on Montage's version of object
                // descriptor.
                var isMontageDataType = type instanceof DataObjectDescriptor || type instanceof ObjectDescriptor;
                return isMontageDataType ?  this._addTriggersForMontageDataType(service, type, prototype, name) :
                                            this._addTriggers(service, type, prototype, requisitePropertyNames);
            }
        },

        _addTriggersForMontageDataType: {
            value: function (service, type, prototype) {
                var triggers = {},
                    names = Object.keys(type.propertyDescriptors),
                    trigger, name, i;
                for (i = 0; (name = names[i]); ++i) {
                    trigger = this.addTrigger(service, type, prototype, name);
                    if (trigger) {
                        triggers[name] = trigger;
                    }
                }
                return triggers;
            }
        },

        _addTriggers: {
            value: function (service, objectDescriptor, prototype, requisitePropertyNames) {
                var triggers = {}, propertyDescriptor, trigger, name, i, n;
                for (i = 0, n = objectDescriptor.propertyDescriptors.length; i < n; i += 1) {
                    propertyDescriptor = objectDescriptor.propertyDescriptors[i];
                    if (!requisitePropertyNames.has(propertyDescriptor.name)) {
                        name = propertyDescriptor.name;
                        trigger = this.addTrigger(service, objectDescriptor, prototype, name);
                        if (trigger) {
                            triggers[name] = trigger;
                        }
                    }
                }
                return triggers;
            }
        },

        /**
         * @method
         * @argument {DataService} service
         * @argument {Object} prototype
         * @argument {string} name
         * @returns {?DataTrigger}
         */
        addTrigger: {
            value: function (service, type, prototype, name) {
                // This function was split into two to provide backwards compatibility
                // to existing Montage data projects.  Future montage data projects
                // should base their object descriptors on Montage's version of object
                // descriptor.
                var isMontageDataType = type instanceof DataObjectDescriptor || type instanceof ObjectDescriptor;
                return isMontageDataType ?  this._addTriggerForMontageDataType(service, type, prototype, name) :
                                            this._addTrigger(service, type, prototype, name);
            }
        },

        _addTriggerForMontageDataType: {
            value: function (service, type, prototype, name) {
                var descriptor = type.propertyDescriptors[name],
                    trigger;
                if (descriptor && descriptor.isRelationship) {
                    trigger = Object.create(this._getTriggerPrototype(service));
                    trigger._objectPrototype = prototype;
                    trigger._propertyName = name;
                    trigger._isGlobal = descriptor.isGlobal;
                    Montage.defineProperty(prototype, name, {
                        get: function () {
                            return trigger._getValue(this);
                        },
                        set: function (value) {
                            trigger._setValue(this, value);
                        }
                    });
                }
                return trigger;
            }
        },

        /**
         * #Performance #ToDO: Here we're creating a trigger instance right away
         * when we could do it only when the defineProperty get/set are called.
         * This means this method wouldn't return the trigger which is added
         * by caller in service._getTriggersForObject. Instead, when created in the
         * get/set, we would added it to the service's ._getTriggersForObject.
         * First draft is bellow, working with bugs, need baking
         *
         * @private
         * @method
         * @argument {DataService} service
         * @argument {ObjectDescriptor} objectDescriptor
         * @argument {Object} prototype
         * @argument {String} name
         * @returns {DataTrigger}
         */
        _createTrigger: {
            value: function(service, objectDescriptor, prototype, name, propertyDescriptor) {
                var trigger = Object.create(this._getTriggerPrototype(service)),
                    serviceTriggers = service._dataObjectTriggers.get(objectDescriptor);
                trigger._objectPrototype = prototype;
                trigger._propertyName = name;
                trigger._isGlobal = propertyDescriptor.isGlobal;
                if(!serviceTriggers) {
                    serviceTriggers = {};
                    service._dataObjectTriggers.set(objectDescriptor,serviceTriggers);
                }
                serviceTriggers[name] = trigger;
                return trigger;
            }
        },
        _addTrigger: {
            value: function (service, objectDescriptor, prototype, name) {
                var descriptor = objectDescriptor.propertyDescriptorForName(name),
                    trigger;
                if (descriptor) {
                    trigger = Object.create(this._getTriggerPrototype(service));
                    trigger._objectPrototype = prototype;
                    trigger._propertyName = name;
                    trigger._isGlobal = descriptor.isGlobal;
                    if (descriptor.definition) {
                        Montage.defineProperty(prototype, name, {
                            get: function () {
                                if (!this.getBinding(name)) {
                                    this.defineBinding(name, {"<-": descriptor.definition});
                                }
                                return trigger._getValue(this);
                                // return (trigger||(trigger = DataTrigger._createTrigger(service, objectDescriptor, prototype, name,descriptor)))._getValue(this);
                            },
                            set: function (value) {
                                trigger._setValue(this, value);
                                // (trigger||(trigger = DataTrigger._createTrigger(service, objectDescriptor, prototype, name,descriptor)))._setValue(this, value);
                            }
                        });
                    } else {
                        Montage.defineProperty(prototype, name, {
                            get: function () {
                                return trigger._getValue(this);
                                // return (trigger||(trigger = DataTrigger._createTrigger(service, objectDescriptor, prototype, name,descriptor)))._getValue(this);
                            },
                            set: function (value) {
                                trigger._setValue(this, value);
                                // (trigger||(trigger = DataTrigger._createTrigger(service, objectDescriptor, prototype, name,descriptor)))._setValue(this, value);
                            }
                        });
                    }
                        trigger = Object.create(this._getTriggerPrototype(service));
                        trigger._objectPrototype = prototype;
                        trigger._propertyName = name;
                        trigger._isGlobal = descriptor.isGlobal;

                }
                return trigger;
            }
        },

        /**
         * To avoid having each trigger hold a reference to the service it uses, all
         * triggers that use a service are derived from a prototype that contains
         * this references. See [_service]{@link DataTrigger#_service} for details.
         *
         * @private
         * @method
         * @argument {DataService} service
         * @returns {DataTrigger}
         */
        _getTriggerPrototype: {
            value: function (service) {
                var trigger = this._triggerPrototypes && this._triggerPrototypes.get(service);
                if (!trigger) {
                    trigger = new this();
                    trigger._service = service;
                    this._triggerPrototypes = this._triggerPrototypes || new WeakMap();
                    this._triggerPrototypes.set(service, trigger);
                }
                return trigger;
            }
        },

        /**
         * @method
         * @argument {Object.<string, DataTrigger>} triggers
         * @argument {Object} prototype
         */
        removeTriggers: {
            value: function (triggers, prototype) {
                var triggerNames = Object.keys(triggers),
                    name, i;
                for (i = 0; (name = triggerNames[i]); ++i) {
                    this.removeTrigger(triggers[name], prototype, name);
                }
            }
        },

        /**
         * @method
         * @argument {DataTrigger} trigger
         * @argument {Object} prototype
         */
        removeTrigger: {
            value: function (trigger, prototype) {
                if (trigger) {
                    delete prototype[trigger.name];
                }
            }
        }

    });