/**
 * 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 (i = 0; i < n; ++i) {
            b = multiBindDescr[i];

            if (b && (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 && 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 && (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 && 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 (i = 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 (i = 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 (i = 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();
        }
    }
});