/**
 * This class manages bindings for a `Session` or `ViewModel`.
 * @private
 */
Ext.define('Ext.app.bind.AbstractStub', {
    extend: 'Ext.util.Schedulable',

    requires: [
        'Ext.app.bind.Binding'
    ],

    children: null,

    depth: 0,

    generation: 1,

    kind: 10,

    parent: null,

    constructor: function(owner, name) {
        var me = this;

        /**
         * @property {Ext.data.Session/Ext.app.ViewModel} owner
         * This property is set at creation of ths stub and should not be changed.
         * @readonly
         */
        me.owner = owner;
        me.name = name;

        me.callParent();
    },

    destroy: function() {
        var me = this,
            children = me.children,
            bindings = me.bindings,
            len, i, key;

        if (bindings) {
            for (i = 0, len = bindings.length; i < len; ++i) {
                bindings[i].destroy(true);
            }
        }

        for (key in children) {
            children[key].destroy();
        }

        if (me.scheduled) {
            me.unschedule();
        }

        me.callParent();
    },

    add: function(child) {
        var me = this;

        (me.children || (me.children = {}))[child.name] = child;

        child.depth = me.depth + 1;
        child.parent = me;
    },

    getChild: function(path) {
        var pathArray = Ext.isString(path) ? path.split('.') : path;

        if (pathArray && pathArray.length) {
            return this.descend(pathArray, 0);
        }

        return this;
    },

    getFullName: function() {
        var me = this,
            name = me.fullName,
            parent = me.parent,
            s;

        if (!name) {
            name = me.name || me.id;

            if (parent && (s = parent.getFullName())) {
                name = ((s.charAt(s.length - 1) !== ':') ? s + '.' : s) + name;
            }

            me.fullName = name;
        }

        return name;
    },

    getSession: function() {
        var owner = this.owner;

        return owner.isSession ? owner : owner.getSession();
    },

    bind: function(callback, scope, options) {
        var me = this,
            binding = new Ext.app.bind.Binding(me, callback, scope, options),
            bindings = (me.bindings || (me.bindings = []));

        binding.depth = me.depth;
        bindings.push(binding);

        return binding;
    },

    getValue: function() {
        return this.isAvailable() ? this.getRawValue() : null;
    },

    graft: function(replacement) {
        var me = this,
            bindings = me.bindings,
            name = me.name,
            i;

        // Clear these so that when we call destroy we won't damage anything:
        me.parent = me.bindings = null;
        me.destroy(); // we may be scheduled

        replacement.depth = me.depth;
        replacement.bindings = bindings;
        replacement.generation = me.generation + 1;
        replacement.name = name;
        replacement.id = me.id;
        replacement.path = me.path;

        // Now for the fun part...
        if (bindings) {
            for (i = bindings.length; i-- > 0;) {
                bindings[i].stub = replacement;
            }
        }

        return replacement;
    },

    isDescendantOf: function(item) {
        var parent;

        for (parent = this; parent = parent.parent;) { // eslint-disable-line no-cond-assign
            if (parent === item) {
                return true;
            }
        }

        return false;
    },

    isAvailable: function() {
        return true;
    },

    isLoading: function() {
        return false;
    },

    onSchedule: function() {
        var i, len, binding, bindings, p;

        // When a stub changes, say "foo.bar.baz" we may need to notify bindings on our
        // parents "foo.bar" and "foo", This is true especially when these are targets of
        // links. To economize on this we require that bindings that want to be notified
        // of changes to sub-properties of their target set the "deep" property to true.
        for (p = this.parent; p; p = p.parent) {
            bindings = p.bindings;

            if (bindings) {
                for (i = 0, len = bindings.length; i < len; ++i) {
                    binding = bindings[i];

                    if (binding.deep && !binding.scheduled) {
                        binding.schedule();
                    }
                }
            }
        }
    },

    react: function() {
        var bindings = this.bindings,
            binding, i, len;

        if (bindings) {
            for (i = 0, len = bindings.length; i < len; ++i) {
                binding = bindings[i];

                if (!binding.scheduled) {
                    binding.schedule();
                }
            }
        }
    },

    unbind: function(binding) {
        var bindings = this.bindings;

        if (bindings && bindings.length) {
            Ext.Array.remove(bindings, binding);
        }
    },

    privates: {
        collect: function() {
            var children = this.children,
                bindings = this.bindings,
                totalCount = 0,
                count = 0,
                child, key;

            if (children) {
                for (key in children) {
                    child = children[key];
                    count = child.collect();

                    if (count === 0) {
                        // The child (and any deep children) have no bindings,
                        // so we can consider it a dead node.
                        child.destroy();
                        delete children[key];
                    }

                    totalCount += count;
                }
            }

            if (bindings) {
                totalCount += bindings.length;
            }

            return totalCount;
        },

        getScheduler: function() {
            var owner = this.owner;

            return owner && owner.getScheduler();
        },

        sort: function() {
            var parent = this.parent;

            if (parent) {
                // We sort our parent first because if it is something like a link we need
                // it to determine the value of the root-level property before we can dot
                // our way into it. This is especially important for formulas that might
                // throw errors if the links have not published results before they run.
                this.scheduler.sortItem(parent);
            }

            // Schedulable#sort === emptyFn
            // me.callParent();
        }
    }
});