/**
 * This class is created to manage a multi-bind against a `ViewModel`.
 */
Ext.define('Ext.app.bind.Multi', {
    extend: 'Ext.app.bind.BaseBinding',
 
    isMultiBinding: true,
 
    missing: 1,
 
    // Multi binds have to be deep. We construct a single object/array and we only
    // ever fire by notifying with that value which will never change. As such, we
    // need to notify any child bindings so they can check if their individual
    // bindings have changed.
    deep: true,
 
    /**
     * @cfg {Boolean} trackStatics
     * This option tracks for static branches of the root object which can be pruned using
     * {@link #pruneStaticKeys}. This can be useful to only get the dynamic parts of a multi bind:
     *
     *      {
     *          a: 1,
     *          b: '{someBind}',
     *          c: ['a', 'b', 'c'],
     *          d: ['a', 'b', '{someBind}'],
     *          e: {
     *              y: 1,
     *              z: 2
     *          },
     *          f: {
     *              y: 1,
     *              z: '{someBind}'
     *          }
     *      }
     *
     *      // Will produce
     *      {
     *          b: value,
     *          d: ['a', 'b', value],
     *          f: {
     *              y: 1,
     *              z: value
     *          }
     *      }
     * @private
     * @since 5.1.0
     */
 
    constructor: function(descriptor, owner, callback, scope, options) {
        var me = this,
            trackStatics = options && options.trackStatics;
 
        me.callParent([ owner, callback, scope, options ]);
 
        me.bindings = [];
        me.literal = descriptor.$literal;
 
        if (descriptor.constructor === Object) {
            if (trackStatics) {
                me.staticKeys = [];
            }
 
            me.addObject(descriptor, me.lastValue = {}, me.staticKeys);
        }
        else {
            me.addArray(descriptor, me.lastValue = []);
        }
 
        // We started at missing == 1 so that no immediate callbacks would hit 0 before
        // adding all bindings... so now we decrement by 1 to balance things and see if
        // we are at 0.
        if (! --me.missing && !me.scheduled) {
            me.schedule();
        }
    },
 
    destroy: function() {
        var me = this;
 
        me.bindings = Ext.destroy(me.bindings);
 
        me.callParent();
    },
 
    add: function(descriptor, data, property) {
        var me = this,
            owner = me.owner,
            bindings = me.bindings,
            method = me.literal
                ? (descriptor.reference ? 'bindEntity' : 'bindExpression')
                : 'bind',
            binding, depth;
 
        ++me.missing;
 
        /* eslint-disable indent */
        binding = owner[method](descriptor,
            function(value) {
                data[property] = value;
 
                if (binding.calls === 1) {
                    --me.missing;
                }
 
                if (!me.missing && !me.scheduled) {
                    me.schedule();
                }
            },
            // TODO - split bind options between us and the sub-binds (pass null for now)
            me, null);
        /* eslint-enable indent */
 
        depth = binding.depth;
 
        if (!bindings.length || depth < me.depth) {
            me.depth = depth;
        }
 
        bindings.push(binding);
 
        return !this.isBindingStatic(binding);
    },
 
    addArray: function(multiBindDescr, array) {
        var me = this,
            n = multiBindDescr.length,
            hasDynamic = false,
            dynamic, b, i;
 
        for (= 0; i < n; ++i) {
            b = multiBindDescr[i];
 
            if (&& (b.reference || Ext.isString(b))) {
                dynamic = me.add(b, array, i);
            }
            else if (Ext.isArray(b)) {
                dynamic = me.addArray(b, array[i] = []);
            }
            else if (&& b.constructor === Object) {
                dynamic = me.addObject(b, array[i] = {});
            }
            else {
                array[i] = b;
                dynamic = false;
            }
 
            hasDynamic = hasDynamic || dynamic;
        }
 
        return hasDynamic;
    },
 
    addObject: function(multiBindDescr, object, staticKeys) {
        var me = this,
            hasDynamic = false,
            dynamic, b, name;
 
        for (name in multiBindDescr) {
            b = multiBindDescr[name];
 
            if (&& (b.reference || Ext.isString(b))) {
                dynamic = me.add(b, object, name);
            }
            else if (Ext.isArray(b)) {
                dynamic = me.addArray(b, object[name] = []);
            }
            else if (&& b.constructor === Object) {
                dynamic = me.addObject(b, object[name] = {});
            }
            else {
                object[name] = b;
                dynamic = false;
            }
 
            if (staticKeys && !dynamic) {
                staticKeys.push(name);
            }
 
            hasDynamic = hasDynamic || dynamic;
        }
 
        return hasDynamic;
    },
 
    getFullName: function() {
        var me = this,
            fullName = me.fullName,
            bindings = me.bindings,
            length = bindings.length,
            i;
 
        if (!fullName) {
            fullName = '@[';
 
            for (= 0; i < length; ++i) {
                if (i) {
                    fullName += ',';
                }
 
                fullName += bindings[i].getFullName();
            }
 
            fullName += ']';
 
            me.fullName = fullName;
        }
 
        return fullName;
    },
 
    getRawValue: function() {
        return this.lastValue;
    },
 
    isDescendantOf: function() {
        return false;
    },
 
    isLoading: function() {
        var bindings = this.bindings,
            n = bindings.length;
 
        for (; n-- > 0;) {
            if (bindings[n].isLoading()) {
                return true;
            }
        }
 
        return false;
    },
 
    isAvailable: function() {
        var bindings = this.bindings,
            n = bindings.length;
 
        for (; n-- > 0;) {
            if (bindings[n].isAvailable()) {
                return true;
            }
        }
 
        return false;
    },
 
    isBindingStatic: function(binding) {
        return binding.isTemplateBinding && binding.isStatic;
    },
 
    isStatic: function() {
        var bindings = this.bindings,
            len = bindings.length,
            i, binding;
 
        for (= 0; i < len; ++i) {
            binding = bindings[i];
 
            if (!this.isBindingStatic(binding)) {
                return false;
            }
        }
 
        return true;
    },
 
    pruneStaticKeys: function() {
        var value = Ext.apply({}, this.lastValue),
            keys = this.staticKeys,
            len = keys.length,
            i;
 
        for (= 0; i < len; ++i) {
            delete value[keys[i]];
        }
 
        return value;
    },
 
    react: function() {
        this.notify(this.lastValue);
    },
 
    refresh: function() {
        // @TODO
    },
 
    privates: {
        sort: function() {
            this.scheduler.sortItems(this.bindings);
 
            // Schedulable#sort === emptyFn
            // me.callParent();
        }
    }
});