/**
 * 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 (i 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 (i = 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[n - 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);
            }
        }
    }
});