/**
 * Plugin (ptype = 'rowwidget') that adds the ability to second row body in a grid which expands/contracts.
 *
 * The expand/contract behavior is configurable to react on clicking of the column, double click of the row, and/or hitting enter while a row is selected.
 *
 * The expansion row may contain a {@link #cfg-widget} which is primed with the record of the corresponding grid row.
 * The widget's {@link Ext.Component#cfg-defaultBindProperty defaultBindProperty} property is set to the record.
 */
Ext.define('Ext.grid.plugin.RowWidget', {
    extend: 'Ext.grid.plugin.RowExpander',
    mixins: [
        'Ext.mixin.Identifiable',
        'Ext.mixin.StyleCacher'
    ],
 
    lockableScope: 'top',
 
    alias: 'plugin.rowwidget',
 
    config: {
        /**
         * @cfg {Object} defaultWidgetUI 
         * A map of xtype to {@link Ext.Component#ui} names to use when using Components in the expansion row.
         */
        defaultWidgetUI: {}
    },
 
    /**
     * @cfg {Object} widget 
     * A config object containing an {@link Ext.Component#cfg-xtype xtype}.
     *
     * This is used to create the widgets or components which are rendered into the expansion row.
     *
     * The associated grid row's record is used to update the widget/component's {@link Ext.Component#defaultBindProperty defaultBindProperty}.
     *
     * Note that if this plugin is applied to a lockable grid, the widget applies to the normal (unlocked) side.
     * See {@link #lockedWidget}
     *
     */
    widget: null,
 
    /**
     * @cfg {Object} [lockedWidget]
     * A config object containing an {@link Ext.Component#cfg-xtype xtype}.
     *
     * This is used to create the widgets or components which are rendered into the expansion row *on the locked side of a lockable grid*.
     */
    lockedWidget: null,
 
    addCollapsedCls: {
        fn: function(out, values, parent) {
            var me = this.rowExpander;
            if (!me.recordsExpanded[values.record.internalId]) {
                values.itemClasses.push(me.rowCollapsedCls);
            }
            this.nextTpl.applyOut(values, out, parent);
        },
 
        // We need a high priority to get in ahead of the outerRowTpl 
        // so we can setup row data 
        priority: 20000
    },
 
    setCmp: function(grid) {
        var me = this,
            features,
            widget;
 
        // Generate a unique class name so we can identify our row element. 
        me.rowIdCls = Ext.id(null, Ext.baseCSSPrefix + 'rowwidget-');
 
        // Keep track of which record internalIds are expanded. 
        me.recordsExpanded = {};
 
        Ext.plugin.Abstract.prototype.setCmp.apply(me, arguments);
 
        widget = me.widget;
        //<debug> 
        if (!widget || widget.isComponent) {
            Ext.raise('RowWidget requires a widget configuration.');
        }
        //</debug> 
        me.widget = widget = Ext.apply({}, widget);
 
        // Apply the default UI for the xtype which is going to feature in the normal side's expansion row. 
        if (!widget.ui) {
            widget.ui = me.getDefaultWidgetUI()[widget.xtype] || 'default';
        }
 
        // If the grid is a lockable assembly, we have to track locked widgets. 
        if (grid.enableLocking && me.lockedWidget) {
            me.lockedWidget = widget = Ext.apply({}, me.lockedWidget);
 
            // Apply the default UI for the xtype which is going to feature in the locked side's expansion row. 
            if (!widget.ui) {
                widget.ui = me.getDefaultWidgetUI()[widget.xtype] || 'default';
            }
        }
 
        features = me.getFeatureConfig(grid);
 
        if (grid.features) {
            grid.features = Ext.Array.push(features, grid.features);
        } else {
            grid.features = features;
        }
        // NOTE: features have to be added before init (before Table.initComponent) 
    },
 
    /**
     * @protected
     * @return {Array} And array of Features or Feature config objects.
     * Returns the array of Feature configurations needed to make the RowWidget work.
     * May be overridden in a subclass to modify the returned array.
     */
    getFeatureConfig: function(grid) {
        var me = this,
            features = [],
            featuresCfg = {
                ftype: 'rowbody',
                rowExpander: me,
                doSync: false,
                rowIdCls: me.rowIdCls,
                bodyBefore: me.bodyBefore,
                recordsExpanded: me.recordsExpanded,
                rowBodyHiddenCls: me.rowBodyHiddenCls,
                rowCollapsedCls: me.rowCollapsedCls,
                setupRowData: me.setupRowData,
                setup: me.setup,
 
                // Do not relay click events into the client grid's row 
                onClick: Ext.emptyFn
            };
 
        features.push(Ext.apply({
            lockableScope: 'normal'
        }, featuresCfg));
 
        // Locked side will need a copy to keep the two DOM structures symmetrical. 
        // A lockedWidget config is available to create content in locked side. 
        // The enableLocking flag is set early in Ext.panel.Table#initComponent if any columns are locked. 
        if (grid.enableLocking) {
            features.push(Ext.apply({
                lockableScope: 'locked'
            }, featuresCfg));
        }
 
        return features;
    },
    
    setupRowData: function(record, rowIndex, rowValues) {
        var me = this.rowExpander;
        
        me.rowBodyFeature = this;
        rowValues.rowBodyCls = me.recordsExpanded[record.internalId] ? '' : me.rowBodyHiddenCls;
    },
 
    bindView: function(view) {
        var me = this;
 
        me.viewListeners = view.on({
            refresh: me.onViewRefresh,
            itemadd: me.onItemAdd,
            scope: me,
            destroyable: true
        });
        Ext.override(view, me.viewOverrides);
    },
 
    destroy: function() {
        var me = this,
            id = me.getId();
 
        me.viewListeners.destroy();
        
        if (me.grid.lockable) {
            me.grid.destroyManagedWidgets(id + '-' + me.lockedView.getId());
            me.grid.destroyManagedWidgets(id + '-' + me.normalView.getId());
        } else {
            me.grid.destroyManagedWidgets(id + '-' + me.view.getId());
        }
        
        me.callParent();
    },
 
    privates: {
        viewOverrides: {
            handleEvent: function(e) {
                // An override applied to the client view so that it ignores events from within the expander row 
                // Ignore all events from within our rowwidget 
                if (e.getTarget('.' + this.rowExpander.rowIdCls, this.body)) {
                    return;
                }
 
                this.callParent([e]);
            },
 
            onFocusEnter: function(e) {
                // An override applied to the client view so that it ignores focus moving into the expander row 
                if (e.event.getTarget('.' + this.rowExpander.rowIdCls, this.body)) {
                    return;
                }
 
                this.callParent([e]);
            },
 
            toggleChildrenTabbability: function(enableTabbing) {
                // An override applied to the client view so that it does not interfere 
                // with tabbability of elements within the expander rows. 
                var focusEl = this.getTargetEl(),
                    rows = this.all,
                    restoreOptions = { skipSelf: true },
                    saveOptions = { skipSelf: true, includeSaved: false },
                    i;
 
                for (= rows.startIndex; i <= rows.endIndex; i++) {
                    // Extract the data row from each row. 
                    // We do not interfere with tabbing in the the expander row. 
                    focusEl = Ext.fly(this.getRow(rows.item(i)));
 
                    if (!focusEl) {
                        continue;
                    }
                    
                    if (enableTabbing) {
                        focusEl.restoreTabbableState(restoreOptions);
                    }
                    else {
                        // Do NOT includeSaved 
                        // Once an item has had tabbability saved, do not increment its save level 
                        focusEl.saveTabbableState(saveOptions);
                    }
                }
            }
        },
 
        destroyLiveWidget: function(recId, widget) {
            widget.destroy();
        },
 
        destroyFreeWidget: function(widget) {
            widget.destroy();
        },
 
        onItemAdd: function(newRecords, startIndex, newItems, view) {
            var me = this,
                len = newItems.length, i,
                record,
                ownerLockable = me.grid.lockable;
 
            // May be multiple widgets being layed out here 
            Ext.suspendLayouts();
 
            for (= 0; i < len; i++) {
                record = newRecords[i];
                if (!record.isNonData && me.recordsExpanded[record.internalId]) {
                    // If any added items are expanded, we will need a syncRowHeights call on next layout 
                    if (ownerLockable) {
                        me.grid.syncRowHeightOnNextLayout = true;
                    }
                    me.addWidget(view, record);
                }
            }
            Ext.resumeLayouts(true);
        },
 
        onViewRefresh: function(view, records) {
            var me = this,
                rows = view.all,
                itemIndex, recordIndex;
 
            Ext.suspendLayouts();
            for (itemIndex = rows.startIndex, recordIndex = 0; itemIndex <= rows.endIndex; itemIndex++, recordIndex++) {
                if (me.recordsExpanded[records[recordIndex].internalId]) {
                    me.addWidget(view, records[recordIndex]);
                }
            }
            Ext.resumeLayouts(true);
        },
 
        returnFalse: function() {
            return false;
        },
 
        /**
         * Returns if possible the widget currently associated with the passed record within the passed view.
         *
         * Note that if the record is not currently in the rendered block, *or*, it has never been expanded
         * then there will not be a widget associated with that `record/view` context.
         * @param {type} view The view for which to return the widget
         * @param {type} record The record for which to return the widget
         * @return {me.lockedLiveWidgets/me.liveWidgets}
         */
        getWidget: function(view, record) {
            var me = this,
                result,
                widget;
 
            if (record) {
                widget = me.grid.lockable && view === me.lockedView ? me.lockedWidget : me.widget;
                if (widget) {
                    result = me.grid.createManagedWidget(view, me.getId() + '-' + view.getId(), widget, record);
                    result.measurer = me;
                    result.ownerLayout = view.componentLayout;
                }
            }
 
            return result;
        },
 
        addWidget: function(view, record) {
            var me = this,
                target,
                width,
                widget,
                hasAttach = !!me.onWidgetAttach,
                isFixedSize = me.isFixedSize,
                el;
 
            // If the record is non data (placeholder), or not expanded, return 
            if (record.isNonData || !me.recordsExpanded[record.internalId]) {
                return;
            }
 
            target = Ext.fly(view.getNode(record).querySelector(me.rowBodyFeature.innerSelector));
            width = target.getWidth(true) - target.getPadding('lr');
            widget = me.getWidget(view, record);
 
            // Might be no widget if we are handling a lockable grid 
            // and only one side has a widget definition. 
            if (widget) {
                if (hasAttach) {
                    Ext.callback(me.onWidgetAttach, me.scope, [me, widget, record], 0, me);
                }
 
                el = widget.el || widget.element;
                if (el) {
                    target.dom.appendChild(el.dom);
                    if (!isFixedSize && widget.width !== width) {
                        widget.setWidth(width);
                    } else {
                        widget.updateLayout();
                    }
                    widget.reattachToBody();
                } else {
                    if (!isFixedSize) {
                        widget.width = width;
                    }
                    widget.render(target);
                }
                widget.updateLayout();
            }
 
            return widget;
        },
 
        toggleRow: function(rowIdx, record) {
            var me = this,
                // If we are handling a lockable assembly, 
                // handle the normal view first 
                view = me.normalView || me.view,
                rowNode = view.getNode(rowIdx),
                normalRow = Ext.fly(rowNode),
                lockedRow,
                nextBd = normalRow.down(me.rowBodyTrSelector, true),
                wasCollapsed = normalRow.hasCls(me.rowCollapsedCls),
                addOrRemoveCls = wasCollapsed ? 'removeCls' : 'addCls',
                ownerLockable = me.grid.lockable && me.grid,
                widget, vm;
 
            normalRow[addOrRemoveCls](me.rowCollapsedCls);
            Ext.fly(nextBd)[addOrRemoveCls](me.rowBodyHiddenCls);
 
            // All layouts must be coalesced. 
            // Particularly important for locking assemblies which need 
            // to sync row height on the next layout. 
            Ext.suspendLayouts();
 
            // We're expanding 
            if (wasCollapsed) {
                me.recordsExpanded[record.internalId] = true;
                widget = me.addWidget(view, record);
                vm = widget.lookupViewModel();
            } else {
                delete me.recordsExpanded[record.internalId];
                widget = me.getWidget(view, record);
            }
 
            // Sync the collapsed/hidden classes on the locked side 
            if (ownerLockable) {
 
                // Only attempt to toggle lockable side if it is visible. 
                if (ownerLockable.lockedGrid.isVisible()) {
 
                    view = me.lockedView;
 
                    // Process the locked side. 
                    lockedRow = Ext.fly(view.getNode(rowIdx));
                    // Just because the grid is locked, doesn't mean we'll necessarily have a locked row. 
                    if (lockedRow) {
                        lockedRow[addOrRemoveCls](me.rowCollapsedCls);
 
                        // If there is a template for expander content in the locked side, toggle that side too 
                        nextBd = lockedRow.down(me.rowBodyTrSelector, true);
                        Ext.fly(nextBd)[addOrRemoveCls](me.rowBodyHiddenCls);
 
                        // Pass an array if we're in a lockable assembly. 
                        if (wasCollapsed && me.lockedWidget) {
                            widget = [widget, me.addWidget(view, record)];
                        } else {
                            widget = [widget, me.getWidget(view, record)];
                        }
 
                    }
 
                    // We're going to need a layout run to synchronize row heights 
                    ownerLockable.syncRowHeightOnNextLayout = true;
                }
            }
 
            me.view.fireEvent(wasCollapsed ? 'expandbody' : 'collapsebody', rowNode, record, nextBd, widget);
            view.updateLayout();
 
            // Before layouts are resumed, if we have *expanded* the widget row, then ensure bound data 
            // is flushed into the widget so that it assumes its final size. 
            if (vm) {
                vm.notify();
            }
            Ext.resumeLayouts(true);
 
            if (me.scrollIntoViewOnExpand && wasCollapsed) {
                me.grid.ensureVisible(rowIdx);
            }
        }
    }
});