/**
 * This layout implements the column arrangement for {@link Ext.form.CheckboxGroup} and
 * {@link Ext.form.RadioGroup}. It groups the component's sub-items into columns
 * based on the component's {@link Ext.form.CheckboxGroup#columns columns} and
 * {@link Ext.form.CheckboxGroup#vertical} config properties.
 */
Ext.define('Ext.layout.container.CheckboxGroup', {
    extend: 'Ext.layout.container.Container',
    alias: ['layout.checkboxgroup'],
 
    /**
     * @cfg {Boolean} [autoFlex=true]
     * By default,  CheckboxGroup allocates all available space to the configured columns
     * meaning that column are evenly spaced across the container.
     *
     * To have each column only be wide enough to fit the container Checkboxes (or Radios),
     * set `autoFlex` to `false`
     */
    autoFlex: true,
 
    type: 'checkboxgroup',
    
    createsInnerCt: true,
 
    childEls: [
        'innerCt'
    ],
 
    /* eslint-disable indent, max-len */
    renderTpl:
        '<table id="{ownerId}-innerCt" data-ref="innerCt" class="' + Ext.baseCSSPrefix + 'table-plain" cellpadding="0"' +
            'role="presentation" style="{tableStyle}">' +
            '<tbody role="presentation">' +
                '<tr role="presentation">' +
                    '<tpl for="columns">' +
                        '<td class="{parent.colCls}" valign="top" style="{style}" role="presentation">' +
                            '{% this.renderColumn(out,parent,xindex-1) %}' +
                        '</td>' +
                    '</tpl>' +
                '</tr>' +
            '</tbody>' +
        '</table>',
    /* eslint-enable indent, max-len */
 
    lastOwnerItemsGeneration: null,
 
    initLayout: function() {
        var me = this,
            owner = me.owner;
 
        me.columnsArray = Ext.isArray(owner.columns);
        me.autoColumns = !owner.columns || owner.columns === 'auto';
        
        // Auto layout is always horizontal
        if (!me.autoColumns) {
            // ... but one column is always vertical
            me.vertical = owner.vertical ||
                          (owner.columns === 1 || owner.columns.length === 1);
        }
 
        me.callParent();
    },
 
    beginLayout: function(ownerContext) {
        var me = this,
            autoFlex = me.autoFlex,
            innerCtStyle = me.innerCt.dom.style,
            totalFlex = 0,
            flexedCols = 0,
            columns, numCols, i, width, cwidth;
 
        me.callParent(arguments);
 
        columns = me.rowNodes[0].children;
        ownerContext.innerCtContext = ownerContext.getEl('innerCt', me);
 
        // The columns config may be an array of widths. Any value < 1 is taken to be a fraction:
        if (!ownerContext.widthModel.shrinkWrap) {
            numCols = columns.length;
 
            // If columns is an array of numeric widths
            if (me.columnsArray) {
 
                // first calculate total flex
                for (= 0; i < numCols; i++) {
                    width = me.owner.columns[i];
 
                    if (width < 1) {
                        totalFlex += width;
                        flexedCols++;
                    }
                }
 
                // now apply widths
                for (= 0; i < numCols; i++) {
                    width = me.owner.columns[i];
 
                    if (width < 1) {
                        cwidth = ((width / totalFlex) * 100) + '%';
                    }
                    else {
                        cwidth = width + 'px';
                    }
 
                    columns[i].style.width = cwidth;
                }
            }
 
            // Otherwise it's the *number* of columns, so distributed the widths evenly
            else {
                for (= 0; i < numCols; i++) {
                    // autoFlex: true will automatically calculate % widths
                    // autoFlex: false allows the table to decide (shrinkWrap, in effect)
                    // on a per-column basis
                    cwidth = autoFlex ? (1 / numCols * 100) + '%' : '';
                    columns[i].style.width = cwidth;
                    flexedCols++;
                }
            }
 
            // no flexed cols -- all widths are fixed
            if (!flexedCols) {
                innerCtStyle.tableLayout = 'fixed';
                innerCtStyle.width = '';
            // some flexed cols -- need to fix some
            }
            else if (flexedCols < numCols) {
                innerCtStyle.tableLayout = 'fixed';
                innerCtStyle.width = '100%';
            // let the table decide
            }
            else {
                innerCtStyle.tableLayout = 'auto';
 
                // if autoFlex, fill available space, else compact down
                if (autoFlex) {
                    innerCtStyle.width = '100%';
                }
                else {
                    innerCtStyle.width = '';
                }
            }
 
        }
        else {
            innerCtStyle.tableLayout = 'auto';
            innerCtStyle.width = '';
        }
    },
 
    cacheElements: function() {
        var me = this;
 
        // Grab defined childEls
        me.callParent();
 
        me.rowNodes = me.innerCt.query('tr', true);
        
        // There always should be at least one row
        me.tBodyNode = me.rowNodes[0].parentNode;
    },
 
    /*
     * Just wait for the child items to all lay themselves out in the width we are configured
     * to make available to them. Then we can measure our height.
     */
    calculate: function(ownerContext) {
        var me = this,
            targetContext, widthShrinkWrap, heightShrinkWrap, shrinkWrap, table, targetPadding;
 
        // The column nodes are widthed using their own width attributes, we just need to wait
        // for all children to have arranged themselves in that width, and then collect our height.
        if (!ownerContext.getDomProp('containerChildrenSizeDone')) {
            me.done = false;
        }
        else {
            targetContext = ownerContext.innerCtContext;
            widthShrinkWrap = ownerContext.widthModel.shrinkWrap;
            heightShrinkWrap = ownerContext.heightModel.shrinkWrap;
            shrinkWrap = heightShrinkWrap || widthShrinkWrap;
            table = targetContext.el.dom;
            targetPadding = shrinkWrap && targetContext.getPaddingInfo();
 
            if (widthShrinkWrap) {
                ownerContext.setContentWidth(table.offsetWidth + targetPadding.width, true);
            }
 
            if (heightShrinkWrap) {
                ownerContext.setContentHeight(table.offsetHeight + targetPadding.height, true);
            }
        }
    },
 
    doRenderColumn: function(out, renderData, columnIndex) {
        // Careful! This method is bolted on to the renderTpl so all we get for context is
        // the renderData! The "this" pointer is the renderTpl instance!
 
        var me = renderData.$layout,
            owner = me.owner,
            columnCount = renderData.columnCount,
            items = owner.items.items,
            itemCount = items.length,
            item, itemIndex, rowCount, increment, tree;
 
        // Example:
        //      columnCount = 3
        //      items.length = 10
 
        if (owner.vertical) {
            //    For vertical layouts we're using only one row
            //    with items rendered "vertically" into table cells.
            //    This is to ensure proper DOM order for native
            //    keyboard navigation.
            //
            //        0   1   2
            //      +---+---+---+
            //    0 | 0 | 4 | 8 |
            //    1 | 1 | 5 | 9 |
            //    2 | 2 | 6 |   |
            //    3 | 3 | 7 |   |
            //      +---+---+---+
 
            rowCount = Math.ceil(itemCount / columnCount); // = 4
            itemIndex = columnIndex * rowCount;
            itemCount = Math.min(itemCount, itemIndex + rowCount);
            increment = 1;
        }
        else {
            //    For horizontal layouts we're using table with rows
            //    and cells, each cell holding one item.
            //
            //        0   1   2
            //      +---+---+---+
            //    0 | 0 | 1 | 2 |
            //      +---+---+---+
            //    1 | 3 | 4 | 5 |
            //      +---+---+---+
            //    2 | 6 | 7 | 8 |
            //      +---+---+---+
            //    3 | 9 |   |   |
            //      +---+---+---+
 
            itemIndex = columnIndex;
            increment = columnCount;
        }
 
        for (; itemIndex < itemCount; itemIndex += increment) {
            item = items[itemIndex];
            me.configureItem(item);
            tree = item.getRenderTree();
            Ext.DomHelper.generateMarkup(tree, out);
        }
    },
 
    /**
     * Returns the number of columns in the checkbox group.
     * @private
     */
    getColumnCount: function() {
        var me = this,
            owner = me.owner,
            ownerColumns = owner.columns;
 
        // Our columns config is an array of numeric widths.
        // Calculate our total width
        if (me.columnsArray) {
            return ownerColumns.length;
        }
 
        if (Ext.isNumber(ownerColumns)) {
            return ownerColumns;
        }
 
        return owner.items.length;
    },
 
    getItemSizePolicy: function(item) {
        return this.autoSizePolicy;
    },
 
    getRenderData: function() {
        var me = this,
            data = me.callParent(),
            owner = me.owner,
            columns = me.getColumnCount(),
            autoFlex = me.autoFlex,
            totalFlex = 0,
            flexedCols = 0,
            width, column, cwidth, i;
 
        // calculate total flex
        if (me.columnsArray) {
            for (= 0; i < columns; i++) {
                width = me.owner.columns[i];
 
                if (width < 1) {
                    totalFlex += width;
                    flexedCols++;
                }
            }
        }
 
        data.colCls = owner.groupCls;
        data.columnCount = columns;
 
        data.columns = [];
 
        for (= 0; i < columns; i++) {
            column = (data.columns[i] = {});
 
            if (me.columnsArray) {
                width = me.owner.columns[i];
 
                if (width < 1) {
                    cwidth = ((width / totalFlex) * 100) + '%';
                }
                else {
                    cwidth = width + 'px';
                }
 
                column.style = 'width:' + cwidth;
            }
            else {
                column.style = 'width:' + (1 / columns * 100) + '%';
                flexedCols++;
            }
        }
 
        /* eslint-disable indent, multiline-ternary, no-multi-spaces */
        // If the columns config was an array of column widths, allow table to auto width
        data.tableStyle = !flexedCols            ? 'table-layout:fixed;'
                        : (flexedCols < columns) ? 'table-layout:fixed;width:100%'
                        : (autoFlex)             ? 'table-layout:auto;width:100%'
                        :                          'table-layout:auto;';
        /* eslint-enable indent, multiline-ternary, no-multi-spaces */
 
        return data;
    },
 
    // Always valid. beginLayout ensures the encapsulating elements of all children
    // are in the correct place
    isValidParent: Ext.returnTrue,
 
    setupRenderTpl: function(renderTpl) {
        this.callParent(arguments);
 
        renderTpl.renderColumn = this.doRenderColumn;
    },
 
    renderChildren: function() {
        var me = this,
            generation = me.owner.items.generation;
 
        if (me.lastOwnerItemsGeneration !== generation) {
            me.lastOwnerItemsGeneration = generation;
            me.renderItems(me.getLayoutItems());
        }
    },
 
    /**
     * Iterates over all passed items, ensuring they are rendered.  If the items
     * are already rendered, also determines if the items are in the proper place in the dom.
     * @protected
     */
    renderItems: function(items) {
        var me = this,
            itemCount = items.length,
            item, rowCount, columnCount, rowIndex, columnIndex, i;
 
        if (itemCount) {
            Ext.suspendLayouts();
            
            // We operate on "virtual" row and column counts here, which is the same
            // as the actual DOM structure for horizontal layouts but is quite different
            // for vertical layouts.
            if (me.autoColumns) {
                columnCount = itemCount;
                rowCount = 1;
            }
            else {
                columnCount = me.columnsArray ? me.owner.columns.length : me.owner.columns;
                rowCount = Math.ceil(itemCount / columnCount);
            }
            
            for (= 0; i < itemCount; i++) {
                item = items[i];
                rowIndex = me.getRenderRowIndex(i, rowCount, columnCount);
                columnIndex = me.getRenderColumnIndex(i, rowCount, columnCount);
 
                if (!item.rendered) {
                    me.renderItem(item, rowIndex, columnIndex);
                }
                else if (!me.isItemAtPosition(item, rowIndex, columnIndex)) {
                    me.moveItem(item, rowIndex, columnIndex);
                }
            }
 
            me.pruneRows(rowCount, columnCount);
 
            Ext.resumeLayouts(true);
        }
    },
 
    isItemAtPosition: function(item, rowIndex, columnIndex) {
        return item.el.dom === this.getItemNodeAt(rowIndex, columnIndex);
    },
 
    getRenderColumnIndex: function(itemIndex, rowCount, columnCount) {
        if (this.vertical) {
            return Math.floor(itemIndex / rowCount);
        }
        else {
            return itemIndex % columnCount;
        }
    },
 
    getRenderRowIndex: function(itemIndex, rowCount, columnCount) {
        if (this.vertical) {
            return itemIndex % rowCount;
        }
        else {
            return Math.floor(itemIndex / columnCount);
        }
    },
 
    getItemNodeAt: function(rowIndex, columnIndex) {
        var column = this.getColumnNodeAt(rowIndex, columnIndex);
        
        return this.vertical ? column.children[rowIndex] : column.children[0];
    },
 
    getRowNodeAt: function(rowIndex) {
        var me = this,
            row;
        
        // Vertical layout uses only one row with several columns,
        // each column containing one or more items, thus simulating "rows"
        rowIndex = me.vertical ? 0 : rowIndex;
        row = me.rowNodes[rowIndex];
        
        if (!row) {
            row = me.rowNodes[rowIndex] = document.createElement('tr');
            row.role = 'presentation';
            me.tBodyNode.appendChild(row);
        }
        
        return row;
    },
    
    getColumnNodeAt: function(rowIndex, columnIndex, row) {
        var column;
        
        row = row || this.getRowNodeAt(rowIndex);
        column = row.children[columnIndex];
        
        if (!column) {
            column = Ext.fly(row).appendChild({
                tag: 'td',
                cls: this.owner.groupCls,
                vAlign: 'top',
                role: 'presentation'
            }, true);
        }
        
        return column;
    },
    
    pruneRows: function(rowCount, columnCount) {
        var me = this,
            rows = me.tBodyNode.children,
            columns, row, column, i, j;
        
        rowCount = me.vertical ? 1 : rowCount;
        
        while (rows.length > rowCount) {
            row = rows[rows.length - 1];
            
            while (row.children.length) {
                Ext.get(row.children[0]).destroy();
            }
            
            // We don't create Element instances for rows
            row.parentNode.removeChild(row);
        }
        
        for (= rowCount - 1; i >= 0; i--) {
            row = rows[i];
            columns = row.children;
            
            while (columns.length > columnCount) {
                column = columns[columns.length - 1];
                Ext.get(column).destroy();
            }
            
            // We only prune empty cells on 2nd and subsequent rows;
            // the first row needs to have all cells up to columnCount
            // to establish the structure.
            if (> 0) {
                for (= columns.length - 1; j >= 0; j--) {
                    column = columns[j];
                    
                    // We only need to test for the last cells that can be empty
                    // due to item removal. As soon as we reach a non-empty column
                    // there's no point in continuing the loop.
                    if (column.children.length === 0) {
                        Ext.get(column).destroy();
                    }
                    else {
                        break;
                    }
                }
            }
        }
    },
    
    /**
     * Renders the given Component into the specified row and column
     * @param {Ext.Component} item The Component to render
     * @param {number} rowIndex row index
     * @param {number} columnIndex column index
     * @private
     */
    renderItem: function(item, rowIndex, columnIndex) {
        var me = this,
            column, itemIndex;
 
        me.configureItem(item);
 
        itemIndex = me.vertical ? rowIndex : 0;
        column = Ext.get(me.getColumnNodeAt(rowIndex, columnIndex));
        
        item.render(column, itemIndex);
    },
 
    /**
     * Moves the given already rendered Component to the specified row and column
     * @param {Ext.Component} item The Component to move
     * @param {number} rowIndex row index
     * @param {number} columnIndex column index
     * @private
     */
    moveItem: function(item, rowIndex, columnIndex) {
        var me = this,
            column, itemIndex, targetNode;
        
        itemIndex = me.vertical ? rowIndex : 0;
        column = me.getColumnNodeAt(rowIndex, columnIndex);
        targetNode = column.children[itemIndex];
        
        column.insertBefore(item.el.dom, targetNode || null);
    },
    
    destroy: function() {
        if (this.owner.rendered) {
            // eslint-disable-next-line vars-on-top
            var target = this.getRenderTarget(),
                cells, i, len;
            
            if (target) {
                cells = target.query('.' + this.owner.groupCls, false);
                
                for (= 0, len = cells.length; i < len; i++) {
                    cells[i].destroy();
                }
            }
        }
        
        this.callParent();
    }
});