/**
 * @private
 * This class encapsulates a row of managed Widgets/Components when a WidgetColumn, or
 * RowWidget plugin is used in a grid.
 *
 * The instances are recycled, and this class holds instances which are derendered so that they can
 * be moved back into newly rendered rows.
 *
 * Developers should not use this class.
 *
 */
Ext.define('Ext.grid.RowContext', {
    constructor: function(config) {
        Ext.apply(this, config);
 
        this.widgets = {};
 
        /**
         * @property {Number} usage
         * Tracks the views that are using this row context. Each view is represented by
         * a bit position. The normal view of a locked grid (or the only view in a
         * non-locking grid) uses bit 0 (i.e., `1`). The locked view uses bit 1 (i.e., `2`).
         *
         * This value will be in the range of 0-3 (0 when unused by a view, 3 when used by
         * both normal and locking views).
         * @since 7.1
         * @private
         */
        this.usage = 0;
    },
 
    setRecord: function(record, recordIndex) {
        var viewModel = this.viewModel;
 
        this.record = record;
        this.recordIndex = recordIndex;
 
        if (viewModel) {
            viewModel.set('record', record);
            viewModel.set('recordIndex', recordIndex);
        }
    },
 
    attach: function(view) {
        var was = this.usage;
 
        this.usage |= view.usageBitMask;
 
        return !was;
    },
 
    detach: function(view) {
        var me = this,
            widgets = me.widgets,
            usageBitMask = view.usageBitMask,
            viewModel = me.viewModel,
            focusEl, free, widget, widgetId;
 
        if (!(me.usage & usageBitMask)) {
            return false;
        }
 
        me.usage &= ~usageBitMask;
        free = !me.usage;
 
        if (free) {
            me.record = null;
 
            if (viewModel) {
                viewModel.set('record');
                viewModel.set('recordIndex');
            }
        }
 
        // All the widgets this RowContext manages must be blurred
        // and moved into the detached body to save them from garbage collection.
        for (widgetId in widgets) {
            widget = widgets[widgetId];
 
            // Only remove widgets owned by this view...
            if (!view.isAncestor(widget)) {
                // NOTE: We cannot do something like "view.el.contains(widget.el)" in
                // trees because collapsing nodes de-render content too early.
                continue;
            }
 
            // Focusables in a grid must not be tabbable by default when they get put back in.
            focusEl = widget.getFocusEl();
 
            if (focusEl) {
                // Widgets are reused so we must reset their tabbable state
                // regardless of their visibility.
                // For example, when removing rows in IE8 we're attaching
                // the nodes to a document-fragment which itself is invisible,
                // so isTabbable() returns false. Next time when we're reusing
                // this widget it will be attached to the document with its
                // tabbable state unreset, which might lead to undesired results.
                if (focusEl.isTabbable(true)) {
                    focusEl.saveTabbableState({
                        includeHidden: true
                    });
                }
 
                // Some browsers do not deliver a focus change upon DOM removal.
                // Force the issue here.
                focusEl.blur();
            }
 
            if (widget.rendered) {
                widget.detachFromBody();
            }
        }
 
        return free;
    },
 
    getWidget: function(view, ownerId, widgetCfg) {
        var me = this,
            widgets = me.widgets || (me.widgets = {}),
            ownerGrid = me.ownerGrid,
            rowViewModel = ownerGrid.rowViewModel,
            rowVM = me.viewModel,
            result;
 
        // Only spin up an attached ViewModel when we instantiate our first managed Widget
        // which uses binding.
        if ((widgetCfg.bind || rowViewModel) && !rowVM) {
            if (typeof rowViewModel === 'string') {
                rowViewModel = {
                    type: rowViewModel
                };
            }
 
            me.viewModel = rowVM = Ext.Factory.viewModel(Ext.merge({
                parent: ownerGrid.getRowContextViewModelParent(),
                data: {
                    record: me.record,
                    recordIndex: me.recordIndex
                }
            }, rowViewModel));
        }
 
        if (!(result = widgets[ownerId])) {
            result = widgets[ownerId] = Ext.widget(Ext.apply({
                ownerCmp: view,
                _rowContext: me,
                // This will spin up a VM on the grid if we don't have one that
                // will be shared across all instances. If we don't have a rowVM
                // or a viewmodel on the created object, we don't  need it, but
                // we can't really tell until we create an instance.
                $vmParent: rowVM || ownerGrid.getRowContextViewModelParent(),
                initInheritedState: me.initInheritedStateHook,
                lookupViewModel: me.lookupViewModelHook
            }, widgetCfg));
 
            result.$fromLocked = !!view.isLockedView;
 
            // Components initialize binding on render.
            // Widgets in finishRender which will not be called in this case.
            // That is only called when rendered by a layout.
            if (result.isWidget) {
                result.initBindable();
            }
            else {
                // Components store an Element reference to their container element.
                // For ordinary components that is not a problem since usually
                // that element is also referenced by Container instance, and gets
                // cleaned up when Container is destroyed. However for row widgets
                // the container element is a cell; cells do not get Element instances
                // and View is not going to clean them up.
                // So we have to clean up explicitly when the widget is destroyed
                // to avoid orphan Element instances in Ext.cache.
                result.collectContainerElement = true;
            }
        }
 
        return result;
    },
 
    getWidgets: function() {
        var widgets = this.widgets,
            id,
            result = [];
 
        for (id in widgets) {
            result.push(widgets[id]);
        }
 
        return result;
    },
 
    handleWidgetViewChange: function(view, ownerId) {
        var widget = this.widgets[ownerId];
 
        if (widget) {
            // In this particular case poking the ownerCmp doesn't really have any significance here
            // since users will typically be interacting at the grid level. However, this is more
            // for the sake of correctness. We don't need to do anything other
            // than poke the reference.
            widget.ownerCmp = view;
            widget.$fromLocked = !!view.isLockedView;
        }
    },
 
    destroy: function() {
        var me = this,
            widgets = me.widgets,
            widgetId,
            widget;
 
        for (widgetId in widgets) {
            widget = widgets[widgetId];
            widget._rowContext = null;
            widget.destroy();
        }
 
        Ext.destroy(me.viewModel);
 
        me.callParent();
    },
 
    privates: {
        initInheritedStateHook: function(inheritedState, inheritedStateInner) {
            var vmParent = this.$vmParent;
 
            this.self.prototype.initInheritedState.call(this, inheritedState, inheritedStateInner);
 
            if (!inheritedState.hasOwnProperty('viewModel') && vmParent) {
                inheritedState.viewModel = vmParent;
            }
        },
 
        lookupViewModelHook: function(skipThis) {
            var ret = skipThis ? null : this.getViewModel();
 
            if (!ret) {
                ret = this.$vmParent || null;
            }
 
            return ret;
        }
    }
});