/**
 * A mixin that adds `dirty` config and `dirtychange` event to a component (typically a
 * `field` or `form`).
 * @private
 * @since 7.0
 */
Ext.define('Ext.field.Dirty', {
    extend: 'Ext.Mixin',
 
    /**
     * @event dirtychange
     * Fires when a change in the component's {@link #cfg-dirty} state is detected.
     *
     * For containers, this event will be fired on a short delay in some cases.
     *
     * @param {Ext.Component} this 
     * @param {Boolean} dirty True if the component is now dirty.
     * @since 7.0
     */
 
    mixinConfig: {
        id: 'dirtyfield',
 
        after: {
            _fixReference: 'fixDirtyState'
        }
    },
 
    config: {
        /**
         * @cfg {Boolean} bubbleDirty
         * Set to `false` to disable dirty states affecting ancestor containers such as
         * `fieldpanel` or `formpanel`. The dirty state of such containers is based on the
         * presence of dirty descendants. In some cases, however, it may be desired to
         * hide the dirty state of one of these containers from its ancestor containers.
         * @since 7.0
         */
        bubbleDirty: true,
 
        /**
         * @cfg {Boolean} dirty
         * This config property describes the modified state of this component. In most
         * cases this config's value is maintained by the component and should be considered
         * readonly. The class implementor should be the only one to call the setter.
         *
         * For containers, this config will be updated on a short delay in some cases.
         * @since 7.0
         */
        dirty: {
            lazy: true,
 
            $value: false
        }
    },
 
    dirty: false,
 
    _childDirtyState: null,
 
    /**
     * This method is called by descendants that use this mixin when their `dirty` state
     * changes.
     * @param {Boolean} dirty The dirty state of the descendant component.
     * @private
     */
    adjustChildDirtyCount: function(dirty) {
        var me = this,
            childDirtyState = me._childDirtyState;
 
        if (childDirtyState) {
            // Once a hierarchy change occurs, our childDirtyState is nulled out, so we
            // just wait for the fixup pass.
            if (childDirtyState.ready) {
                childDirtyState.counter += dirty ? 1 : -1;
 
                me.setDirty(!!childDirtyState.counter);
            }
            else if (dirty) {
                // When a parent (this object) is not ready, we simply count the number
                // of dirty children. We are presently between calls of beginSyncChildDirty
                // and finishSyncChildDirty.
                ++childDirtyState.counter;
            }
        }
    },
 
    /**
     * This method is called when the component hierarchy has changed and the current set
     * of descendants will be reasserting their `dirty` state. This method is only called
     * on `nameHolder` containers.
     * @private
     */
    beginSyncChildDirty: function() {
        this._childDirtyState = { counter: 0, ready: false };
    },
 
    /**
     * This method is called when the component hierarchy has changed after the current set
     * of descendants has reasserted their `dirty` state. This method is only called on
     * `nameHolder` containers.
     * @private
     */
    finishSyncChildDirty: function() {
        var me = this,
            childDirtyState = me._childDirtyState,
            dirty = !!childDirtyState.counter;
 
        if (dirty !== me.dirty) {
            me.setDirty(dirty);
        }
        else if (dirty) {
            me.informParentDirty(dirty);
        }
 
        childDirtyState.ready = true;
    },
 
    /**
     * @private
     */
    fireDirtyChange: function() {
        this.fireEvent('dirtychange', this, this.dirty);
    },
 
    /**
     * This method is called after `_fixReference()` during the reference sync sweep. We
     * need to inform our parent if we are a leaf component and if we are dirty. If we are
     * a `nameHolder` then we'll inform the parent in `finishSyncChildDirty`.
     * @private
     */
    fixDirtyState: function() {
        var me = this;
 
        if (!me._childDirtyState && me.dirty) {
            me.informParentDirty(true);
        }
    },
 
    informParentDirty: function(dirty) {
        var me = this,
            parent = me.getBubbleDirty() && me.lookupNameHolder(),
            childDirtyState = me._childDirtyState,
            parentChildDirtyState = parent && parent._childDirtyState;
 
        if (parentChildDirtyState) {
            if (childDirtyState) {
                // Four possible states:
                //
                //                   Parent
                //    Child      !ready  ready
                //     !ready       1      2
                //     ready        3      4
                //
                // 1. Neither parent nor child are ready. This happens when the child
                //    is the first to receive finishSyncChildDirty and its updateDirty
                //    get tickled. The parent is still counting its dirty children, so
                //    the child just sends up its dirty state.
                // 2. The parent is ready but not the child. This happens when the child
                //    receives the finishSyncChildDirty after the parent. In this case,
                //    we do not want to inform the parent of a transition to !dirty since
                //    it would decrement its counter.
                // 3. The child has changed dirty state after finishSyncChildDirty was
                //    called (maybe from a grandchild hitting case 2) but the parent has
                //    not received finishSyncChildDirty. As with case 1, just inform.
                // 4. Normal ready state, so just inform.
 
                if (!childDirtyState.ready && parentChildDirtyState.ready) { // case 2
                    if (!dirty) {
                        return;
                    }
                }
            }
            // else the child is not a container/nameHolder (it has no children), so
            // we always inform the parent...
 
            parent.adjustChildDirtyCount(dirty, me);
        }
    },
 
    invalidateChildDirty: function() {
        this._childDirtyState = null;
    },
 
    isDirty: function() {
        // This method is intended for containers of fields. Ext.field.Field has its
        // own isDirty that is designed to handle value-possessing components.
        if (Ext.referencesDirty) {
            Ext.fixReferences();
        }
 
        return this.getDirty();
    },
 
    updateDirty: function(dirty) {
        var me = this;
 
        me.dirty = dirty;
 
        if (!me.isDirtyInitializing) {
            if (me.fireEvent) {
                me.fireDirtyChange();
            }
 
            me.informParentDirty(dirty);
        }
    }
});