/*
 * This file is part of lanterna (https://github.com/mabe02/lanterna).
 * 
 * lanterna is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * Copyright (C) 2010-2020 Martin Berglund
 */
package com.googlecode.lanterna.gui2;

import com.googlecode.lanterna.TerminalPosition;
import com.googlecode.lanterna.TerminalSize;

import java.util.*;

/**
 * This emulates the behaviour of the GridLayout in SWT (as opposed to the one in AWT/Swing). I originally ported the
 * SWT class itself but due to licensing concerns (the eclipse license is not compatible with LGPL) I was advised not to
 * do that. This is a partial implementation and some of the semantics have changed, but in general it works the same
 * way so the SWT documentation will generally match.
 * <p>
 * You use the {@code GridLayout} by specifying a number of columns you want your grid to have and then when you add
 * components, you assign {@code LayoutData} to these components using the different static methods in this class
 * ({@code createLayoutData(..)}). You can set components to span both rows and columns, as well as defining how to
 * distribute the available space.
 */
public class GridLayout implements LayoutManager {
    /**
     * The enum is used to specify where in a grid cell a component should be placed, in the case that the preferred
     * size of the component is smaller than the space in the cell. This class will generally use two alignments, one
     * for horizontal and one for vertical.
     */
    public enum Alignment {
        /**
         * Place the component at the start of the cell (horizontally or vertically) and leave whatever space is left
         * after the preferred size empty.
         */
        BEGINNING,
        /**
         * Place the component at the middle of the cell (horizontally or vertically) and leave the space before and
         * after empty.
         */
        CENTER,
        /**
         * Place the component at the end of the cell (horizontally or vertically) and leave whatever space is left
         * before the preferred size empty.
         */
        END,
        /**
         * Force the component to be the same size as the table cell
         */
        FILL,
        ;
    }

    static class GridLayoutData implements LayoutData {
        final Alignment horizontalAlignment;
        final Alignment verticalAlignment;
        final boolean grabExtraHorizontalSpace;
        final boolean grabExtraVerticalSpace;
        final int horizontalSpan;
        final int verticalSpan;

        private GridLayoutData(
                Alignment horizontalAlignment,
                Alignment verticalAlignment,
                boolean grabExtraHorizontalSpace,
                boolean grabExtraVerticalSpace,
                int horizontalSpan,
                int verticalSpan) {

            if(horizontalSpan < 1 || verticalSpan < 1) {
                throw new IllegalArgumentException("Horizontal/Vertical span must be 1 or greater");
            }

            this.horizontalAlignment = horizontalAlignment;
            this.verticalAlignment = verticalAlignment;
            this.grabExtraHorizontalSpace = grabExtraHorizontalSpace;
            this.grabExtraVerticalSpace = grabExtraVerticalSpace;
            this.horizontalSpan = horizontalSpan;
            this.verticalSpan = verticalSpan;
        }
    }

    private static final GridLayoutData DEFAULT = new GridLayoutData(
            Alignment.BEGINNING,
            Alignment.BEGINNING,
            false,
            false,
            1,
            1);

    /**
     * Creates a layout data object for {@code GridLayout}:s that specify the horizontal and vertical alignment for the
     * component in case the cell space is larger than the preferred size of the component
     * @param horizontalAlignment Horizontal alignment strategy
     * @param verticalAlignment Vertical alignment strategy
     * @return The layout data object containing the specified alignments
     */
    public static LayoutData createLayoutData(Alignment horizontalAlignment, Alignment verticalAlignment) {
        return createLayoutData(horizontalAlignment, verticalAlignment, false, false);
    }

    /**
     * Creates a layout data object for {@code GridLayout}:s that specify the horizontal and vertical alignment for the
     * component in case the cell space is larger than the preferred size of the component. This method also has fields
     * for indicating that the component would like to take more space if available to the container. For example, if
     * the container is assigned is assigned an area of 50x15, but all the child components in the grid together only
     * asks for 40x10, the remaining 10 columns and 5 rows will be empty. If just a single component asks for extra
     * space horizontally and/or vertically, the grid will expand out to fill the entire area and the text space will be
     * assigned to the component that asked for it.
     *
     * @param horizontalAlignment Horizontal alignment strategy
     * @param verticalAlignment Vertical alignment strategy
     * @param grabExtraHorizontalSpace If set to {@code true}, this component will ask to be assigned extra horizontal
     *                                 space if there is any to assign
     * @param grabExtraVerticalSpace If set to {@code true}, this component will ask to be assigned extra vertical
     *                                 space if there is any to assign
     * @return The layout data object containing the specified alignments and size requirements
     */
    public static LayoutData createLayoutData(
            Alignment horizontalAlignment,
            Alignment verticalAlignment,
            boolean grabExtraHorizontalSpace,
            boolean grabExtraVerticalSpace) {

        return createLayoutData(horizontalAlignment, verticalAlignment, grabExtraHorizontalSpace, grabExtraVerticalSpace, 1, 1);
    }

    /**
     * Creates a layout data object for {@code GridLayout}:s that specify the horizontal and vertical alignment for the
     * component in case the cell space is larger than the preferred size of the component. This method also has fields
     * for indicating that the component would like to take more space if available to the container. For example, if
     * the container is assigned is assigned an area of 50x15, but all the child components in the grid together only
     * asks for 40x10, the remaining 10 columns and 5 rows will be empty. If just a single component asks for extra
     * space horizontally and/or vertically, the grid will expand out to fill the entire area and the text space will be
     * assigned to the component that asked for it. It also puts in data on how many rows and/or columns the component
     * should span.
     *
     * @param horizontalAlignment Horizontal alignment strategy
     * @param verticalAlignment Vertical alignment strategy
     * @param grabExtraHorizontalSpace If set to {@code true}, this component will ask to be assigned extra horizontal
     *                                 space if there is any to assign
     * @param grabExtraVerticalSpace If set to {@code true}, this component will ask to be assigned extra vertical
     *                                 space if there is any to assign
     * @param horizontalSpan How many "cells" this component wants to span horizontally
     * @param verticalSpan How many "cells" this component wants to span vertically
     * @return The layout data object containing the specified alignments, size requirements and cell spanning
     */
    public static LayoutData createLayoutData(
            Alignment horizontalAlignment,
            Alignment verticalAlignment,
            boolean grabExtraHorizontalSpace,
            boolean grabExtraVerticalSpace,
            int horizontalSpan,
            int verticalSpan) {

        return new GridLayoutData(
                horizontalAlignment,
                verticalAlignment,
                grabExtraHorizontalSpace,
                grabExtraVerticalSpace,
                horizontalSpan,
                verticalSpan);
    }

    /**
     * This is a shortcut method that will create a grid layout data object that will expand its cell as much as is can
     * horizontally and make the component occupy the whole area horizontally and center it vertically, spanning 1 cell.
     * @return Layout data object with the specified span and horizontally expanding as much as it can
     */
    public static LayoutData createHorizontallyFilledLayoutData() {
        return createLayoutData(
                Alignment.FILL,
                Alignment.CENTER,
                true,
                false,
                1,
                1);
    }

    /**
     * This is a shortcut method that will create a grid layout data object that will expand its cell as much as is can
     * horizontally and make the component occupy the whole area horizontally and center it vertically
     * @param horizontalSpan How many cells to span horizontally
     * @return Layout data object with the specified span and horizontally expanding as much as it can
     */
    public static LayoutData createHorizontallyFilledLayoutData(int horizontalSpan) {
        return createLayoutData(
                Alignment.FILL,
                Alignment.CENTER,
                true,
                false,
                horizontalSpan,
                1);
    }

    /**
     * This is a shortcut method that will create a grid layout data object that will expand its cell as much as is can
     * vertically and make the component occupy the whole area vertically and center it horizontally
     * @param horizontalSpan How many cells to span vertically
     * @return Layout data object with the specified span and vertically expanding as much as it can
     */
    public static LayoutData createHorizontallyEndAlignedLayoutData(int horizontalSpan) {
        return createLayoutData(
                Alignment.END,
                Alignment.CENTER,
                true,
                false,
                horizontalSpan,
                1);
    }

    private final int numberOfColumns;
    private int horizontalSpacing;
    private int verticalSpacing;
    private int topMarginSize;
    private int bottomMarginSize;
    private int leftMarginSize;
    private int rightMarginSize;

    private boolean changed;

    /**
     * Creates a new {@code GridLayout} with the specified number of columns. Initially, this layout will have a
     * horizontal spacing of 1 and vertical spacing of 0, with a left and right margin of 1.
     * @param numberOfColumns Number of columns in this grid
     */
    public GridLayout(int numberOfColumns) {
        this.numberOfColumns = numberOfColumns;
        this.horizontalSpacing = 1;
        this.verticalSpacing = 0;
        this.topMarginSize = 0;
        this.bottomMarginSize = 0;
        this.leftMarginSize = 1;
        this.rightMarginSize = 1;
        this.changed = true;
    }

    /**
     * Returns the horizontal spacing, i.e. the number of empty columns between each cell
     * @return Horizontal spacing
     */
    public int getHorizontalSpacing() {
        return horizontalSpacing;
    }

    /**
     * Sets the horizontal spacing, i.e. the number of empty columns between each cell
     * @param horizontalSpacing New horizontal spacing
     * @return Itself
     */
    public GridLayout setHorizontalSpacing(int horizontalSpacing) {
        if(horizontalSpacing < 0) {
            throw new IllegalArgumentException("Horizontal spacing cannot be less than 0");
        }
        this.horizontalSpacing = horizontalSpacing;
        this.changed = true;
        return this;
    }

    /**
     * Returns the vertical spacing, i.e. the number of empty columns between each row
     * @return Vertical spacing
     */
    public int getVerticalSpacing() {
        return verticalSpacing;
    }

    /**
     * Sets the vertical spacing, i.e. the number of empty columns between each row
     * @param verticalSpacing New vertical spacing
     * @return Itself
     */
    public GridLayout setVerticalSpacing(int verticalSpacing) {
        if(verticalSpacing < 0) {
            throw new IllegalArgumentException("Vertical spacing cannot be less than 0");
        }
        this.verticalSpacing = verticalSpacing;
        this.changed = true;
        return this;
    }

    /**
     * Returns the top margin, i.e. number of empty rows above the first row in the grid
     * @return Top margin, in number of rows
     */
    public int getTopMarginSize() {
        return topMarginSize;
    }

    /**
     * Sets the top margin, i.e. number of empty rows above the first row in the grid
     * @param topMarginSize Top margin, in number of rows
     * @return Itself
     */
    public GridLayout setTopMarginSize(int topMarginSize) {
        if(topMarginSize < 0) {
            throw new IllegalArgumentException("Top margin size cannot be less than 0");
        }
        this.topMarginSize = topMarginSize;
        this.changed = true;
        return this;
    }

    /**
     * Returns the bottom margin, i.e. number of empty rows below the last row in the grid
     * @return Bottom margin, in number of rows
     */
    public int getBottomMarginSize() {
        return bottomMarginSize;
    }

    /**
     * Sets the bottom margin, i.e. number of empty rows below the last row in the grid
     * @param bottomMarginSize Bottom margin, in number of rows
     * @return Itself
     */
    public GridLayout setBottomMarginSize(int bottomMarginSize) {
        if(bottomMarginSize < 0) {
            throw new IllegalArgumentException("Bottom margin size cannot be less than 0");
        }
        this.bottomMarginSize = bottomMarginSize;
        this.changed = true;
        return this;
    }

    /**
     * Returns the left margin, i.e. number of empty columns left of the first column in the grid
     * @return Left margin, in number of columns
     */
    public int getLeftMarginSize() {
        return leftMarginSize;
    }

    /**
     * Sets the left margin, i.e. number of empty columns left of the first column in the grid
     * @param leftMarginSize Left margin, in number of columns
     * @return Itself
     */
    public GridLayout setLeftMarginSize(int leftMarginSize) {
        if(leftMarginSize < 0) {
            throw new IllegalArgumentException("Left margin size cannot be less than 0");
        }
        this.leftMarginSize = leftMarginSize;
        this.changed = true;
        return this;
    }

    /**
     * Returns the right margin, i.e. number of empty columns right of the last column in the grid
     * @return Right margin, in number of columns
     */
    public int getRightMarginSize() {
        return rightMarginSize;
    }

    /**
     * Sets the right margin, i.e. number of empty columns right of the last column in the grid
     * @param rightMarginSize Right margin, in number of columns
     * @return Itself
     */
    public GridLayout setRightMarginSize(int rightMarginSize) {
        if(rightMarginSize < 0) {
            throw new IllegalArgumentException("Right margin size cannot be less than 0");
        }
        this.rightMarginSize = rightMarginSize;
        this.changed = true;
        return this;
    }

    @Override
    public boolean hasChanged() {
        return this.changed;
    }

    @Override
    public TerminalSize getPreferredSize(List<Component> components) {
        TerminalSize preferredSize = TerminalSize.ZERO;
        if(components.isEmpty()) {
            return preferredSize.withRelative(
                    leftMarginSize + rightMarginSize,
                    topMarginSize + bottomMarginSize);
        }

        Component[][] table = buildTable(components);
        table = eliminateUnusedRowsAndColumns(table);

        //Figure out each column first, this can be done independently of the row heights
        int preferredWidth = 0;
        int preferredHeight = 0;
        for(int width: getPreferredColumnWidths(table)) {
            preferredWidth += width;
        }
        for(int height: getPreferredRowHeights(table)) {
            preferredHeight += height;
        }
        preferredSize = preferredSize.withRelative(preferredWidth, preferredHeight);
        preferredSize = preferredSize.withRelativeColumns(leftMarginSize + rightMarginSize + (table[0].length - 1) * horizontalSpacing);
        preferredSize = preferredSize.withRelativeRows(topMarginSize + bottomMarginSize + (table.length - 1) * verticalSpacing);
        return preferredSize;
    }

    @Override
    public void doLayout(TerminalSize area, List<Component> components) {
        //Sanity check, if the area is way too small, just return
        Component[][] table = buildTable(components);
        table = eliminateUnusedRowsAndColumns(table);

        if(area.equals(TerminalSize.ZERO) ||
                table.length == 0 ||
                area.getColumns() <= leftMarginSize + rightMarginSize + ((table[0].length - 1) * horizontalSpacing) ||
                area.getRows() <= bottomMarginSize + topMarginSize + ((table.length - 1) * verticalSpacing)) {
            changed = false;
            return;
        }

        //Adjust area to the margins
        area = area.withRelative(-leftMarginSize - rightMarginSize, -topMarginSize - bottomMarginSize);

        Map<Component, TerminalSize> sizeMap = new IdentityHashMap<>();
        Map<Component, TerminalPosition> positionMap = new IdentityHashMap<>();

        //Figure out each column first, this can be done independently of the row heights
        int[] columnWidths = getPreferredColumnWidths(table);

        //Take notes of which columns we can expand if the usable area is larger than what the components want
        Set<Integer> expandableColumns = getExpandableColumns(table);

        //Next, start shrinking to make sure it fits the size of the area we are trying to lay out on.
        //Notice we subtract the horizontalSpacing to take the space between components into account
        TerminalSize areaWithoutHorizontalSpacing = area.withRelativeColumns(-horizontalSpacing * (table[0].length - 1));
        int totalWidth = shrinkWidthToFitArea(areaWithoutHorizontalSpacing, columnWidths);

        //Finally, if there is extra space, make the expandable columns larger
        while(areaWithoutHorizontalSpacing.getColumns() > totalWidth && !expandableColumns.isEmpty()) {
            totalWidth = grabExtraHorizontalSpace(areaWithoutHorizontalSpacing, columnWidths, expandableColumns, totalWidth);
        }

        //Now repeat for rows
        int[] rowHeights = getPreferredRowHeights(table);
        Set<Integer> expandableRows = getExpandableRows(table);
        TerminalSize areaWithoutVerticalSpacing = area.withRelativeRows(-verticalSpacing * (table.length - 1));
        int totalHeight = shrinkHeightToFitArea(areaWithoutVerticalSpacing, rowHeights);
        while(areaWithoutVerticalSpacing.getRows() > totalHeight && !expandableRows.isEmpty()) {
            totalHeight = grabExtraVerticalSpace(areaWithoutVerticalSpacing, rowHeights, expandableRows, totalHeight);
        }

        //Ok, all constraints are in place, we can start placing out components. To simplify, do it horizontally first
        //and vertically after
        TerminalPosition tableCellTopLeft = TerminalPosition.TOP_LEFT_CORNER;
        for(int y = 0; y < table.length; y++) {
            tableCellTopLeft = tableCellTopLeft.withColumn(0);
            for(int x = 0; x < table[y].length; x++) {
                Component component = table[y][x];
                if(component != null && !positionMap.containsKey(component)) {
                    GridLayoutData layoutData = getLayoutData(component);
                    TerminalSize size = component.getPreferredSize();
                    TerminalPosition position = tableCellTopLeft;

                    int availableHorizontalSpace = 0;
                    int availableVerticalSpace = 0;
                    for (int i = 0; i < layoutData.horizontalSpan && x + i < columnWidths.length; i++) {
                        availableHorizontalSpace += columnWidths[x + i] + (i > 0 ? horizontalSpacing : 0);
                    }
                    for (int i = 0; i < layoutData.verticalSpan; i++) {
                        availableVerticalSpace += rowHeights[y + i]  + (i > 0 ? verticalSpacing : 0);
                    }

                    //Make sure to obey the size restrictions
                    size = size.withColumns(Math.min(size.getColumns(), availableHorizontalSpace));
                    size = size.withRows(Math.min(size.getRows(), availableVerticalSpace));

                    switch (layoutData.horizontalAlignment) {
                        case CENTER:
                            position = position.withRelativeColumn((availableHorizontalSpace - size.getColumns()) / 2);
                            break;
                        case END:
                            position = position.withRelativeColumn(availableHorizontalSpace - size.getColumns());
                            break;
                        case FILL:
                            size = size.withColumns(availableHorizontalSpace);
                            break;
                        default:
                            break;
                    }
                    switch (layoutData.verticalAlignment) {
                        case CENTER:
                            position = position.withRelativeRow((availableVerticalSpace - size.getRows()) / 2);
                            break;
                        case END:
                            position = position.withRelativeRow(availableVerticalSpace - size.getRows());
                            break;
                        case FILL:
                            size = size.withRows(availableVerticalSpace);
                            break;
                        default:
                            break;
                    }

                    sizeMap.put(component, size);
                    positionMap.put(component, position);
                }
                tableCellTopLeft = tableCellTopLeft.withRelativeColumn(columnWidths[x] + horizontalSpacing);
            }
            tableCellTopLeft = tableCellTopLeft.withRelativeRow(rowHeights[y] + verticalSpacing);
        }

        //Apply the margins here
        for(Component component: components) {
            component.setPosition(positionMap.get(component).withRelative(leftMarginSize, topMarginSize));
            component.setSize(sizeMap.get(component));
        }
        this.changed = false;
    }

    private int[] getPreferredColumnWidths(Component[][] table) {
        //actualNumberOfColumns may be different from this.numberOfColumns since some columns may have been eliminated
        int actualNumberOfColumns = table[0].length;
        int[] columnWidths = new int[actualNumberOfColumns];

        //Start by letting all span = 1 columns take what they need
        for(Component[] row: table) {
            for(int i = 0; i < actualNumberOfColumns; i++) {
                Component component = row[i];
                if(component == null) {
                    continue;
                }
                GridLayoutData layoutData = getLayoutData(component);
                if (layoutData.horizontalSpan == 1) {
                    columnWidths[i] = Math.max(columnWidths[i], component.getPreferredSize().getColumns());
                }
            }
        }

        //Next, do span > 1 and enlarge if necessary
        for(Component[] row: table) {
            for(int i = 0; i < actualNumberOfColumns; ) {
                Component component = row[i];
                if(component == null) {
                    i++;
                    continue;
                }
                GridLayoutData layoutData = getLayoutData(component);
                int horizontalSpan = Math.min(layoutData.horizontalSpan, actualNumberOfColumns - i);
                if(horizontalSpan > 1) {
                    int accumWidth = 0;
                    for(int j = i; j < i + horizontalSpan; j++) {
                        accumWidth += columnWidths[j];
                    }

                    int preferredWidth = component.getPreferredSize().getColumns();
                    if(preferredWidth > accumWidth) {
                        int columnOffset = 0;
                        do {
                            columnWidths[i + columnOffset++]++;
                            accumWidth++;
                            if(columnOffset == horizontalSpan) {
                                columnOffset = 0;
                            }
                        }
                        while(preferredWidth > accumWidth);
                    }
                }
                i += horizontalSpan;
            }
        }
        return columnWidths;
    }

    private int[] getPreferredRowHeights(Component[][] table) {
        int numberOfRows = table.length;
        int[] rowHeights = new int[numberOfRows];

        //Start by letting all span = 1 rows take what they need
        int rowIndex = 0;
        for(Component[] row: table) {
            for(Component component : row) {
                if(component == null) {
                    continue;
                }
                GridLayoutData layoutData = getLayoutData(component);
                if(layoutData.verticalSpan == 1) {
                    rowHeights[rowIndex] = Math.max(rowHeights[rowIndex], component.getPreferredSize().getRows());
                }
            }
            rowIndex++;
        }

        //Next, do span > 1 and enlarge if necessary
        for(int x = 0; x < numberOfColumns; x++) {
            for(int y = 0; y < numberOfRows; ) {
                if(x >= table[y].length) {
                    y++;
                    continue;
                }
                Component component = table[y][x];
                if(component == null) {
                    y++;
                    continue;
                }
                GridLayoutData layoutData = getLayoutData(component);
                if(layoutData.verticalSpan > 1) {
                    int accumulatedHeight = 0;
                    for(int i = y; i < y + layoutData.verticalSpan; i++) {
                        accumulatedHeight += rowHeights[i];
                    }

                    int preferredHeight = component.getPreferredSize().getRows();
                    if(preferredHeight > accumulatedHeight) {
                        int rowOffset = 0;
                        do {
                            rowHeights[y + rowOffset++]++;
                            accumulatedHeight++;
                            if(rowOffset == layoutData.verticalSpan) {
                                rowOffset = 0;
                            }
                        }
                        while(preferredHeight > accumulatedHeight);
                    }
                }
                y += layoutData.verticalSpan;
            }
        }
        return rowHeights;
    }

    private Set<Integer> getExpandableColumns(Component[][] table) {
        Set<Integer> expandableColumns = new TreeSet<>();
        Component previousComponent = null;
        for(Component[] row: table) {
            for (int i = 0; i < row.length; i++) {
                if(row[i] == null || row[i] == previousComponent) {
                    continue;
                }
                GridLayoutData layoutData = getLayoutData(row[i]);
                if(layoutData.grabExtraHorizontalSpace) {
                    expandableColumns.add(i);
                }
                previousComponent = row[i];
            }
        }
        return expandableColumns;
    }

    private Set<Integer> getExpandableRows(Component[][] table) {
        Set<Integer> expandableRows = new TreeSet<>();
        Component previousComponent = null;
        if(table.length > 0) {
            for (int columnIndex = 0; columnIndex < table[0].length; columnIndex++) {
                for (int rowIndex = 0; rowIndex < table.length; rowIndex++) {
                    Component cell = table[rowIndex][columnIndex];
                    if (cell == null || cell == previousComponent) {
                        continue;
                    }
                    GridLayoutData layoutData = getLayoutData(cell);
                    if (layoutData.grabExtraVerticalSpace) {
                        expandableRows.add(rowIndex);
                    }
                    previousComponent = cell;
                }
            }
        }
        return expandableRows;
    }

    private int shrinkWidthToFitArea(TerminalSize area, int[] columnWidths) {
        int totalWidth = 0;
        for(int width: columnWidths) {
            totalWidth += width;
        }
        if(totalWidth > area.getColumns()) {
            int columnOffset = 0;
            do {
                if(columnWidths[columnOffset] > 0) {
                    columnWidths[columnOffset]--;
                    totalWidth--;
                }
                if(++columnOffset == columnWidths.length) {
                    columnOffset = 0;
                }
            }
            while(totalWidth > area.getColumns());
        }
        return totalWidth;
    }

    private int shrinkHeightToFitArea(TerminalSize area, int[] rowHeights) {
        int totalHeight = 0;
        for(int height: rowHeights) {
            totalHeight += height;
        }
        if(totalHeight > area.getRows()) {
            int rowOffset = 0;
            do {
                if(rowHeights[rowOffset] > 0) {
                    rowHeights[rowOffset]--;
                    totalHeight--;
                }
                if(++rowOffset == rowHeights.length) {
                    rowOffset = 0;
                }
            }
            while(totalHeight > area.getRows());
        }
        return totalHeight;
    }

    private int grabExtraHorizontalSpace(TerminalSize area, int[] columnWidths, Set<Integer> expandableColumns, int totalWidth) {
        for(int columnIndex: expandableColumns) {
            columnWidths[columnIndex]++;
            totalWidth++;
            if(area.getColumns() == totalWidth) {
                break;
            }
        }
        return totalWidth;
    }

    private int grabExtraVerticalSpace(TerminalSize area, int[] rowHeights, Set<Integer> expandableRows, int totalHeight) {
        for(int rowIndex: expandableRows) {
            rowHeights[rowIndex]++;
            totalHeight++;
            if(area.getColumns() == totalHeight) {
                break;
            }
        }
        return totalHeight;
    }

    private Component[][] buildTable(List<Component> components) {
        List<Component[]> rows = new ArrayList<>();
        List<int[]> hspans = new ArrayList<>();
        List<int[]> vspans = new ArrayList<>();

        int rowCount = 0;
        int rowsExtent = 1;
        Queue<Component> toBePlaced = new LinkedList<>(components);
        while(!toBePlaced.isEmpty() || rowCount < rowsExtent) {
            //Start new row
            Component[] row = new Component[numberOfColumns];
            int[] hspan = new int[numberOfColumns];
            int[] vspan = new int[numberOfColumns];

            for(int i = 0; i < numberOfColumns; i++) {
                if(i > 0 && hspan[i - 1] > 1) {
                    row[i] = row[i-1];
                    hspan[i] = hspan[i - 1] - 1;
                    vspan[i] = vspan[i - 1];
                }
                else if(rowCount > 0 && vspans.get(rowCount - 1)[i] > 1) {
                    row[i] = rows.get(rowCount - 1)[i];
                    hspan[i] = hspans.get(rowCount - 1)[i];
                    vspan[i] = vspans.get(rowCount - 1)[i] - 1;
                }
                else if(!toBePlaced.isEmpty()) {
                    Component component = toBePlaced.poll();
                    GridLayoutData gridLayoutData = getLayoutData(component);

                    row[i] = component;
                    hspan[i] = gridLayoutData.horizontalSpan;
                    vspan[i] = gridLayoutData.verticalSpan;
                    rowsExtent = Math.max(rowsExtent, rowCount + gridLayoutData.verticalSpan);
                }
                else {
                    row[i] = null;
                    hspan[i] = 1;
                    vspan[i] = 1;
                }
            }

            rows.add(row);
            hspans.add(hspan);
            vspans.add(vspan);
            rowCount++;
        }
        return rows.toArray(new Component[rows.size()][]);
    }

    private Component[][] eliminateUnusedRowsAndColumns(Component[][] table) {
        if(table.length == 0) {
            return table;
        }
        //Could make this into a Set, but I doubt there will be any real gain in performance as these are probably going
        //to be very small.
        List<Integer> rowsToRemove = new ArrayList<>();
        List<Integer> columnsToRemove = new ArrayList<>();

        final int tableRows = table.length;
        final int tableColumns = table[0].length;

        //Scan for unnecessary columns
        columnLoop:
        for(int column = tableColumns - 1; column > 0; column--) {
            for(Component[] row : table) {
                if(row[column] != null) {
                    continue columnLoop;
                }
            }
            columnsToRemove.add(column);
        }

        //Scan for unnecessary rows
        rowLoop:
        for(int row = tableRows - 1; row > 0; row--) {
            for(int column = 0; column < tableColumns; column++) {
                if(table[row][column] != null) {
                    continue rowLoop;
                }
            }
            rowsToRemove.add(row);
        }

        //If there's nothing to remove, just return the same
        if(rowsToRemove.isEmpty() && columnsToRemove.isEmpty()) {
            return table;
        }

        //Build a new table with rows & columns eliminated
        Component[][] newTable = new Component[tableRows - rowsToRemove.size()][];
        int insertedRowCounter = 0;
        for(Component[] row : table) {
            Component[] newColumn = new Component[tableColumns - columnsToRemove.size()];
            int insertedColumnCounter = 0;
            for(int column = 0; column < tableColumns; column++) {
                if(columnsToRemove.contains(column)) {
                    continue;
                }
                newColumn[insertedColumnCounter++] = row[column];
            }
            newTable[insertedRowCounter++] = newColumn;
        }
        return newTable;
    }

    private GridLayoutData getLayoutData(Component component) {
        LayoutData layoutData = component.getLayoutData();
        if(layoutData instanceof GridLayoutData) {
            return (GridLayoutData)layoutData;
        }
        else {
            return DEFAULT;
        }
    }
}