/**
 * This class and its derived classes are used to manage access to the properties of an
 * object stored in a `Session`.
 * @private
 */
Ext.define('Ext.app.bind.Stub', {
    extend: 'Ext.app.bind.AbstractStub',
 
    requires: [
        'Ext.app.bind.Binding'
    ],
 
    isStub: true,
 
    dirty: true,
 
    formula: null,
 
    validationKey: 'validation',
 
    constructor: function(owner, name, parent) {
        var me = this,
            path = name;
 
        me.callParent([ owner, name ]);
        me.boundValue = null;
 
        if (parent) {
            parent.add(me);
 
            if (!parent.isRootStub) {
                path = parent.path + '.' + name;
            }
 
            me.checkHadValue();
        }
 
        me.path = path;
    },
 
    destroy: function() {
        var me = this,
            formula = me.formula,
            storeBinding = me.storeBinding;
 
        if (formula) {
            formula.destroy();
        }
 
        if (storeBinding) {
            storeBinding.destroy();
        }
 
        me.detachBound();
 
        me.callParent();
    },
 
    bindValidation: function(callback, scope) {
        var parent = this.parent;
 
        return parent && parent.descend([this.validationKey, this.name]).bind(callback, scope);
    },
 
    bindValidationField: function(callback, scope) {
        var parent = this.parent,
            name = this.name,
            lateBound = typeof callback === 'string',
            ret;
 
        if (parent) {
            ret = parent.bind(function(value) {
                var field = null;
 
                if (value && value.isModel) {
                    field = value.getField(name);
                }
 
                if (lateBound) {
                    scope[callback](field, value, this);
                }
                else {
                    callback.call(scope, field, value, this);
                }
            });
        }
 
        return ret || null;
    },
 
    descend: function(path, index) {
        var me = this,
            children = me.children || (me.children = {}),
            pos = index || 0,
            name = path[pos++],
            ret;
 
        if (!(ret = children[name])) {
            ret = new Ext.app.bind.Stub(me.owner, name, me);
        }
 
        if (pos < path.length) {
            ret = ret.descend(path, pos);
        }
 
        return ret;
    },
 
    getChildValue: function(parentData) {
        var me = this,
            name = me.name,
            bindMappings = me.bindMappings,
            storeMappings = bindMappings.store,
            modelMappings = bindMappings.model,
            ret;
 
        if (!parentData && !Ext.isString(parentData)) {
            // since these forms of falsey values (0, false, etc.) are not things we
            // can index into, this child stub must be null.
            ret = me.hadValue ? null : undefined;
        }
        else {
            ret = me.inspectValue(parentData);
 
            if (!ret) {
                if (parentData.isEntity) {
                    // If we get here, we know it's not an association
                    if (modelMappings[name]) {
                        ret = parentData[modelMappings[name]]();
                    }
                    else {
                        ret = parentData.data[name];
                    }
                }
                else if (parentData.isStore && storeMappings[name]) {
                    ret = parentData[storeMappings[name]]();
                }
                else {
                    ret = parentData[name];
 
                    if (ret === undefined && me.hadValue) {
                        ret = null;
                    }
                }
            }
        }
 
        return ret;
    },
 
    getDataObject: function() {
        var me = this,
            parentData = me.parent.getDataObject(), // RootStub does not get here
            name = me.name,
            ret = parentData ? parentData[name] : null,
            storeMappings = me.bindMappings.store,
            associations;
 
        if (!ret) {
            if (parentData && parentData.isEntity) {
                // Check if the item is an association, if it is, grab it but don't load it.
                associations = parentData.associations;
 
                if (associations && name in associations) {
                    ret = parentData[associations[name].getterName]();
                }
            }
        }
        else if (parentData.isStore && name in storeMappings) {
            ret = parentData[storeMappings[name]]();
        }
 
        if (!ret || !(ret.$className || Ext.isObject(ret))) {
            parentData[name] = ret = {};
 
            // We're implicitly setting a value on the object here
            me.hadValue = true;
 
            // If we're creating the parent data object, invalidate the dirty
            // flag on our children.
            me.invalidate(true, true);
        }
 
        return ret;
    },
 
    getRawValue: function() {
        // NOTE: The RootStub class does not call here so we will *always* have a parent
        // unless dark energy has won and the laws of physics have broken down.
        return this.getChildValue(this.getParentValue());
    },
 
    graft: function(replacement) {
        var me = this,
            parent = me.parent,
            children = me.children,
            name = me.name,
            i, ret;
 
        replacement.parent = parent;
        replacement.children = children;
 
        if (parent) {
            parent.children[name] = replacement;
        }
 
        if (children) {
            for (in children) {
                children[i].parent = replacement;
            }
        }
 
        me.children = null;
 
        replacement.checkHadValue();
 
        ret = me.callParent([ replacement ]);
        ret.invalidate(true, true);
 
        return ret;
    },
 
    isAvailable: function() {
        return this.checkAvailability();
    },
 
    isLoading: function() {
        return !this.checkAvailability(true);
    },
 
    invalidate: function(deep, dirtyOnly) {
        var me = this,
            children = me.children,
            name;
 
        me.dirty = true;
        me.checkHadValue();
 
        if (!dirtyOnly && me.isAvailable()) {
            if (!me.scheduled) {
                // If we have no children, we're a leaf
                me.schedule();
            }
        }
 
        if (deep && children) {
            for (name in children) {
                children[name].invalidate(deep, dirtyOnly);
            }
        }
    },
 
    isReadOnly: function() {
        var formula = this.formula;
 
        return !!(formula && !formula.set);
    },
 
    set: function(value, preventClimb) {
        var me = this,
            parent = me.parent,
            name = me.name,
            formula = me.formula,
            parentData, associations,
            association, formulaStub, setterName;
 
        if (formula && !formula.settingValue && formula.set) {
            formula.setValue(value);
 
            return;
        }
        else if (me.isLinkStub) {
            formulaStub = me.getLinkFormulaStub();
            formula = formulaStub ? formulaStub.formula : null;
 
            if (formula) {
                //<debug>
                if (formulaStub.isReadOnly()) {
                    Ext.raise('Cannot setValue on a readonly formula');
                }
                //</debug>
 
                formula.setValue(value);
 
                return;
            }
        }
 
        // To set a child property, the parent must be an object...
        parentData = parent.getDataObject();
 
        if (parentData.isEntity) {
            associations = parentData.associations;
 
            if (associations && (name in associations)) {
                association = associations[name];
                setterName = association.setterName;
 
                if (setterName) {
                    parentData[setterName](value);
                }
 
                // We may be setting a record here, force the value to recalculate
                me.invalidate(true);
            }
            else {
                // If not an association then it is a data field
                parentData.set(name, value);
            }
 
            // Setting fields or associated records will fire change notifications so we
            // handle the side effects there
        }
        else if ((value && value.constructor === Object) ||
                 !(value === parentData[name] && parentData.hasOwnProperty(name))) {
            // The hasOwnProperty check is important, even though the value might be the same here,
            // that value could exist in a viewmodel above us
            if (preventClimb || !me.setByLink(value)) {
                if (value === undefined) {
                    delete parentData[name];
                }
                else {
                    parentData[name] = value;
                }
 
                me.inspectValue(parentData);
 
                // We have children, but we're overwriting the value with something else, so
                // we need to schedule our children
                me.invalidate(true);
            }
        }
    },
 
    onStoreDataChanged: function() {
        this.invalidate(true);
    },
 
    afterLoad: function(record) {
        this.invalidate(true);
    },
 
    afterCommit: function(record) {
        // Essentially the same as an edit, but we don't know what changed.
        this.afterEdit(record, null);
    },
 
    afterEdit: function(record, modifiedFieldNames) {
        var children = this.children,
            len = modifiedFieldNames && modifiedFieldNames.length,
            associations = record.associations,
            bindMappings = this.bindMappings.model,
            key, i, child, name, ref;
 
        // No point checking anything if we don't have children
        if (children) {
            if (len) {
                // We know what changed, check for it and schedule it.
                for (= 0; i < len; ++i) {
                    name = modifiedFieldNames[i];
                    child = children[name];
 
                    if (!child) {
                        ref = record.fieldsMap[name];
                        ref = ref && ref.reference;
                        child = ref && children[ref.role];
                    }
 
                    if (child) {
                        child.invalidate(true);
                    }
                }
            }
            else {
                // We don't know what changed, so loop over everything.
                // If the child is not an association, then it's a field so we
                // need to trigger them so we can respond to field changes
                for (key in children) {
                    if (!(associations && key in associations)) {
                        children[key].invalidate(true);
                    }
                }
            }
 
            // Whether we know what changed or not, valid/dirty are meta properties so
            // trigger them regardless
            for (key in bindMappings) {
                child = children[key];
 
                if (child) {
                    child.invalidate();
                }
            }
        }
 
        this.invalidate();
    },
 
    afterReject: function(record) {
        // Essentially the same as an edit, but we don't know what changed.
        this.afterEdit(record, null);
    },
 
    afterAssociatedRecordSet: function(record, associated, role) {
        var children = this.children,
            key = role.role;
 
        if (children && key in children) {
            children[key].invalidate(true);
        }
    },
 
    setByLink: function(value) {
        var me = this,
            n = 0,
            ret = false,
            i, link, path, stub, root, name;
 
        for (stub = me; stub; stub = stub.parent) {
            if (stub.isLinkStub) {
                link = stub;
 
                if (n) {
                    for (path = [], i = 0, stub = me; stub !== link; stub = stub.parent) {
                        ++i;
                        path[- i] = stub.name;
                    }
                }
 
                break;
            }
 
            ++n;
        }
 
        stub = null;
 
        if (link) {
            root = link.parent;
            name = link.name;
 
            if (!root.shouldClimb(name)) {
                // Write to root, descend to stub
                stub = root.insertChild(name);
            }
            else {
                stub = link.getTargetStub();
            }
        }
 
        if (stub) {
            // We are a child of a link stub and that stub links to a Stub, so forward the set
            // call over there. This is needed to fire the bindings on that side of the link
            // and that will also arrive back here since we are a linked to it.
            if (path) {
                stub = stub.descend(path);
            }
 
            stub.set(value);
 
            ret = true;
        }
 
        return ret;
 
    },
 
    setFormula: function(formula) {
        var me = this,
            oldFormula = me.formula;
 
        if (oldFormula) {
            oldFormula.destroy();
        }
 
        // The new formula will bind to what it needs and that will schedule it (and then
        // us when it sets our value).
        me.formula = new Ext.app.bind.Formula(me, formula);
    },
 
    react: function() {
        var me = this,
            bound = this.boundValue,
            children = me.children,
            generation;
 
        if (bound) {
            if (bound.isValidation) {
                bound.refresh();
                generation = bound.generation;
 
                // Don't react if we haven't changed
                if (me.lastValidationGeneration === generation) {
                    return;
                }
 
                me.lastValidationGeneration = generation;
            }
            else if (bound.isModel) {
                // At this point we're guaranteed to have a non-validation model
                // Check if we're interested in it, if so, validate it and let
                // the record fire off any changes
                if (children && children[me.validationKey]) {
                    // Trigger validity checks
                    bound.isValid();
                }
            }
        }
 
        this.callParent();
    },
 
    privates: {
        bindMappings: {
            store: {
                count: 'getCount',
                first: 'first',
                last: 'last',
                loading: 'hasPendingLoad',
                totalCount: 'getTotalCount'
            },
            model: {
                dirty: 'isDirty',
                phantom: 'isPhantom',
                valid: 'isValid'
            }
        },
 
        checkAvailability: function(isLoading) {
            var me = this,
                parent = me.parent,
                bindMappings = me.bindMappings,
                name = me.name,
                available = !!(parent && parent.checkAvailability(isLoading)),
                associations, parentValue, value, availableSet;
 
            if (available) {
                parentValue = me.getParentValue();
                value = me.inspectValue(parentValue);
 
                // If we get a value back, it's something we can ask for the loading state
                if (value) {
                    if (isLoading) {
                        available = !value.hasPendingLoad();
                    }
                    else {
                        // If it's a store, it should be always available, even if loading
                        if (value.isStore) {
                            available = true;
                        }
                        else {
                            // If it's a model and it's loading, only available if it's after
                            // the first time
                            available = !value.isLoading() || value.loadCount > 0;
                        }
                    }
                }
                else {
                    if (parentValue) {
                        if (parentValue.isModel) {
                            if (bindMappings.model[name]) {
                                available = !parent.isLoading();
                                availableSet = true;
                            }
                            else {
                                associations = parentValue.associations;
 
                                // At this point, we know the value is not a record or a store,
                                // otherwise something would have been returned from inspectValue.
                                // We also check here that we are not a defined association,
                                // because we don't treat it like a field.
                                // Otherwise, we are a field on a model, so we're never in a loading
                                // state.
                                if (!(associations && name in associations)) {
                                    available = true;
                                    availableSet = true;
                                }
                            }
                        }
                        else if (parentValue.isStore && bindMappings.store[name] &&
                                 name !== 'loading') {
                            available = !parent.isLoading();
                            availableSet = true;
                        }
                    }
 
                    if (!availableSet) {
                        available = me.hadValue || me.getRawValue() !== undefined;
                    }
                }
            }
 
            return available;
        },
 
        checkHadValue: function() {
            if (!this.hadValue) {
                this.hadValue = this.getRawValue() !== undefined;
            }
        },
 
        collect: function() {
            var me = this,
                result = me.callParent(),
                storeBinding = me.storeBinding ? 1 : 0,
                formula = me.formula ? 1 : 0;
 
            return result + storeBinding + formula;
        },
 
        getLinkFormulaStub: function() {
            // Setting the value on a link backed by a formula should set the
            // formula. So we climb the hierarchy until we find the rootStub
            // and set it there if it be a formula.
            var stub = this;
 
            while (stub.isLinkStub) {
                stub = stub.binding.stub;
            }
 
            return stub.formula ? stub : null;
        },
 
        getParentValue: function() {
            var me = this;
 
            // Cache the value of the parent here. Inside onSchedule we clear the value
            // because it may be invalidated.
            if (me.dirty) {
                me.parentValue = me.parent.getValue();
                me.dirty = false;
            }
 
            return me.parentValue;
        },
 
        setStore: function(storeBinding) {
            this.storeBinding = storeBinding;
        },
 
        inspectValue: function(parentData) {
            var me = this,
                name = me.name,
                current = me.boundValue,
                boundValue = null,
                associations, raw, changed, associatedEntity;
 
            if (parentData && parentData.isEntity) {
                associations = parentData.associations;
 
                if (associations && (name in associations)) {
                    boundValue = parentData[associations[name].getterName]();
                }
                else if (name === me.validationKey) {
                    boundValue = parentData.getValidation();
 
                    // Binding a new one, reset the generation
                    me.lastValidationGeneration = null;
                }
            }
            else if (parentData) {
                raw = parentData[name];
 
                if (raw && (raw.isModel || raw.isStore)) {
                    boundValue = raw;
                }
            }
 
            // Check if we have a current binding that changed. If so, we need
            // to detach ourselves from it
            changed = current !== boundValue;
 
            if (changed) {
                if (current) {
                    me.detachBound();
                }
 
                if (boundValue) {
                    if (boundValue.isModel) {
                        boundValue.join(me);
                    }
                    else {
                        // Only want to trigger automatic loading if we've come from an association.
                        // Otherwise leave the user in charge of that.
                        associatedEntity = boundValue.associatedEntity;
 
                        if (associatedEntity && boundValue.autoLoad !== false &&
                            !boundValue.complete && !boundValue.hasPendingLoad()) {
                            boundValue.load();
                        }
 
                        // We only want to listen for the first load, since the actual
                        // store object won't change from then on
                        boundValue.on({
                            scope: me,
 
                            // Capture beginload/load so we can bind to the loading state
                            // of the store. We need load because a load may be unsuccessful
                            // which means datachanged won't fire beginload is used because
                            // it's fired:
                            // a) After we're sure to load (beforeload could be vetoed)
                            // b) After the loading flag is set to true. This is important
                            // because we fire the datachanged handler which needs to check if
                            // the store is available (loading) to publish values.
                            beginload: 'onStoreDataChanged',
                            load: 'onStoreDataChanged',
                            datachanged: 'onStoreDataChanged',
                            destroy: 'onDestroyBound'
                        });
                    }
                }
 
                me.boundValue = boundValue;
            }
 
            return boundValue;
        },
 
        detachBound: function() {
            var me = this,
                current = me.boundValue;
 
            if (current && !current.destroyed) {
                if (current.isModel) {
                    current.unjoin(me);
                }
                else {
                    current.un({
                        scope: me,
                        beginload: 'onStoreDataChanged',
                        load: 'onStoreDataChanged',
                        datachanged: 'onStoreDataChanged',
                        destroy: 'onDestroyBound'
                    });
                }
            }
        },
 
        onDestroyBound: function() {
            if (!this.owner.destroying) {
                this.set(null);
            }
        },
 
        sort: function() {
            var me = this,
                formula = me.formula,
                scheduler = me.scheduler,
                storeBinding = me.storeBinding;
 
            me.callParent();
 
            if (storeBinding) {
                scheduler.sortItem(storeBinding);
            }
 
            if (formula) {
                // Our formula must run before we do so it can set the value on us. Our
                // bindings in turn depend on us so they will be scheduled as part of the
                // current sweep if the formula produces a different result.
                scheduler.sortItem(formula);
            }
        }
    }
});