/*******************************************************************************
 * Copyright (c) 2004, 2017 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/

package org.eclipse.ui.internal.layout;

import java.util.ArrayList;
import java.util.List;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Layout;

/**
 * <p>Instance of this class lay out the control children of a <code>Composite</code>
 * in a grid, using a simple set of rules and a simple API. This class is
 * intended to be more predictable than <code>GridLayout</code> and easier to use than
 * <code>FormLayout</code>, while retaining most of the power of both.</p>
 *
 * <p>The power of a <code>CellLayout</code> lies in the ability to control
 * the size and resizing properties of each row and column. Unlike other layout
 * classes, complex layouts can be created without attaching any layout data to
 * individual controls in the layout. </p>
 *
 * <p>The various subclasses of <code>IColumnInfo</code>
 * can be used to create columns with fixed width, columns whose width is computed
 * from child controls, or width that grows in proportion to the size of other
 * columns. Layouts can be given a default <code>IColumnInfo</code> that will
 * be used to set the size of any column whose properties have not been explicitly
 * set. This is useful for creating layouts where most or all columns have the
 * same properties. Similarly, the subclasses of <code>IRowInfo</code> can be used to
 * control the height of individual rows.</p>
 *
 * <p>For a finer grain of control, <code>CellData</code> objects can be attached
 * to individual controls in the layout. These objects serve a similar function as
 * <code>GridData</code> objects serve for <code>GridLayout</code>. They allow
 * controls to span multiple rows or columns, set the justification of the control
 * within its cell, and allow the user to override the preferred size of the control.
 * </p>
 *
 * <p>In many cases, it is not necessary to attach any layout data to controls in
 * the layout, since the controls can be arranged based on the properties of rows
 * and columns. However, layout data may be attached to individual controls to
 * allow them to span multiple columns or to control their justification within
 * their cell.
 * </p>
 *
 * <p>All the <code>set</code> methods in this class return <code>this</code>, allowing
 * a layout to be created and initialized in a single line of code. For example: </p>
 *
 * <code>
 * Composite myControl = new Composite(parent, SWT.NONE);
 * myControl.setLayout(new CellLayout(2).setMargins(10,10).setSpacing(5,5));
 * </code>
 *
 * @since 3.0
 */
public class CellLayout extends Layout {

    /**
     * Object used to compute the height of rows whose properties have not been
     * explicitly set.
     */
    private Row defaultRowSettings = new Row(false);

    /**
     * Object used to compute the width of columns whose properties have not been
     * explicitly set.
     */
    private Row defaultColSettings = new Row(true);

    /**
     * horizontalSpacing specifies the number of pixels between the right
     * edge of one cell and the left edge of its neighbouring cell to
     * the right.
     *
     * The default value is 5.
     */
    int horizontalSpacing = 5;

    /**
     * verticalSpacing specifies the number of pixels between the bottom
     * edge of one cell and the top edge of its neighbouring cell underneath.
     *
     * The default value is 5.
     */
    int verticalSpacing = 5;

    /**
     * marginWidth specifies the number of pixels of horizontal margin
     * that will be placed along the left and right edges of the layout.
     *
     * The default value is 0.
     */
    public int marginWidth = 5;

    /**
     * marginHeight specifies the number of pixels of vertical margin
     * that will be placed along the top and bottom edges of the layout.
     *
     * The default value is 0.
     */
    public int marginHeight = 5;

    /**
     * Number of columns in this layout, or 0 indicating that the whole layout
     * should be on a single row.
     */
    private int numCols;

    /**
     * List of IColumnInfo. The nth object is used to compute the width of the
     * nth column, or null indicating that the default column should be used.
     */
    private List cols;

    /**
     * List of RowInfo. The nth object is used to compute the height of the
     * nth row, or null indicating that the default row should be used.
     */
    private List rows = new ArrayList(16);

    // Cached information
    private GridInfo gridInfo = new GridInfo();

    private int[] cachedRowMin = null;

    private int[] cachedColMin = null;

    public static int cacheMisses;

    public static int cacheHits;

    private LayoutCache cache = new LayoutCache();

    // End of cached control sizes

    /**
     * Creates the layout
     *
     * @param numCols the number of columns in this layout,
     * or 0 indicating that the whole layout should be on one row.
     */
    public CellLayout(int numCols) {
        super();
        this.numCols = numCols;
        cols = new ArrayList(numCols == 0 ? 3 : numCols);
    }

    /**
     * Sets the amount empty space between cells
     *
     * @param newSpacing a point (x,y) corresponding to the number of pixels of
     * empty space between adjacent columns and rows respectively
     */
    public CellLayout setSpacing(int horizontalSpacing, int verticalSpacing) {
        this.horizontalSpacing = horizontalSpacing;
        this.verticalSpacing = verticalSpacing;

        return this;
    }

    /**
     * Sets the amount empty space between cells
     *
     * @param newSpacing a point (x,y) corresponding to the number of pixels of
     * empty space between adjacent columns and rows respectively
     */
    public CellLayout setSpacing(Point newSpacing) {
        horizontalSpacing = newSpacing.x;
        verticalSpacing = newSpacing.y;
        return this;
    }

    /**
     * Returns the amount of empty space between adjacent cells
     *
     * @return a point (x,y) corresponding to the number of pixels of empty
     * space between adjacent columns and rows respectively
     */
    public Point getSpacing() {
        return new Point(horizontalSpacing, verticalSpacing);
    }

    /**
     * Sets the size of the margin around the outside of the layout.
     *
     * @param marginWidth the size of the margin around the top and
     * bottom of the layout
     * @param marginHeight the size of the margin on the left and right
     * of the layout.
     */
    public CellLayout setMargins(int marginWidth, int marginHeight) {
        this.marginWidth = marginWidth;
        this.marginHeight = marginHeight;
        return this;
    }

    /**
     * Sets the size of the margin around the outside of the layout.
     *
     * @param newMargins point indicating the size of the horizontal and vertical
     * margins, in pixels.
     */
    public CellLayout setMargins(Point newMargins) {
        marginWidth = newMargins.x;
        marginHeight = newMargins.y;
        return this;
    }

    /**
     * Returns the size of the margins around the outside of the layout.
     *
     * @return the size of the outer margins, in pixels.
     */
    public Point getMargins() {
        return new Point(marginWidth, marginHeight);
    }

    /**
     * Sets the default column settings. All columns will use these settings unless
     * they have been explicitly assigned custom settings by setColumn.
     *
     * @param info the properties of all default columns
     * @see setColumn
     */
    public CellLayout setDefaultColumn(Row info) {
        defaultColSettings = info;
        return this;
    }

    /**
     * Sets the column info for the given column number (the leftmost column is column 0).
     * This replaces any existing info for the column. Note that more than one column
     * are allowed to share the same IColumnInfo instance if they have identical properties.
     *
     * @param colNum the column number to modify
     * @param info the properties of the column, or null if this column should use the
     * default properties
     */
    public CellLayout setColumn(int colNum, Row info) {
        while (cols.size() <= colNum) {
            cols.add(null);
        }

        cols.set(colNum, info);

        return this;
    }

    /**
     * Sets the default row settings for this layout. Unless this is overridden
     * for an individual row, all rows will use the default settings.
     *
     * @param info the row info object that should be used to set the size
     * of rows, by default.
     */
    public CellLayout setDefaultRow(Row info) {
        defaultRowSettings = info;

        return this;
    }

    /**
     * Sets the row info for the given rows. The topmost row is row 0. Multiple
     * rows are allowed to share the same RowInfo instance.
     *
     * @param rowNum the row number to set
     * @param info the row info that will control the sizing of the given row,
     * or null if the row should use the default settings for this layout.
     */
    public CellLayout setRow(int rowNum, Row info) {
        while (rows.size() <= rowNum) {
            rows.add(null);
        }

        rows.set(rowNum, info);

        return this;
    }

    /**
     * Returns the row info that controls the size of the given row. Will return
     * the default row settings for this layout if no custom row info has been
     * assigned to the row.
     *
     * @param rowNum
     * @return
     */
    private Row getRow(int rowNum, boolean isHorizontal) {
        if (isHorizontal) {
            if (rowNum >= rows.size()) {
                return defaultRowSettings;
            }

            Row result = (Row) rows.get(rowNum);

            if (result == null) {
                result = defaultRowSettings;
            }

            return result;
		}
		if (rowNum >= cols.size()) {
			return defaultColSettings;
		}

		Row result = (Row) cols.get(rowNum);

		if (result == null) {
			result = defaultColSettings;
		}

		return result;
    }

    /**
     * Initializes the gridInfo object.
     *
     * @param children controls that are being layed out
     */
    private void initGrid(Control[] children) {
        cache.setControls(children);
        gridInfo.initGrid(children, this);
        cachedRowMin = null;
        cachedColMin = null;
    }

    @Override
	protected Point computeSize(Composite composite, int wHint, int hHint,
            boolean flushCache) {
        Control[] children = composite.getChildren();
        initGrid(children);

        if (flushCache) {
            cache.flush();
        }

        // Determine the amount of whitespace (area that cannot be used by controls)
        Point emptySpace = totalEmptySpace();

        int[] heightConstraints = computeConstraints(true);

        int width;
        if (wHint == SWT.DEFAULT) {
            width = preferredSize(heightConstraints, false);
        } else {
            width = wHint - emptySpace.x;
        }

        int height = hHint;
        if (hHint == SWT.DEFAULT) {
            height = preferredSize(
                    computeSizes(heightConstraints, width, false), true);
        } else {
            height = hHint - emptySpace.y;
        }

        Point preferredSize = new Point(width + emptySpace.x, height
                + emptySpace.y);

        // At this point we know the layout's preferred size. Now adjust it
        // if we're smaller than the minimum possible size for the composite.

        // If exactly one dimension of our preferred size is smaller than
        // the minimum size of our composite, then set that dimension to
        // the minimum size and recompute the other dimension (for example,
        // increasing the width to match a shell's minimum width may reduce
        // the height allocated for a wrapping text widget). There is no
        // point in doing this if both dimensions are smaller than the
        // composite's minimum size, since we're already smaller than
        // we need to be.
        Point minimumSize = CellLayoutUtil.computeMinimumSize(composite);

        boolean wider = (preferredSize.x >= minimumSize.x);
        boolean taller = (preferredSize.y >= minimumSize.y);

        if (wider) {
            if (taller) {
                // If we're larger in both dimensions, don't adjust the minimum
                // size.
                return preferredSize;
            }
			// If our preferred height is smaller than the minimum height,
			// recompute the preferred width using the minimum height
			return computeSize(composite, wHint, minimumSize.y, false);
        }
		if (taller) {
			// If our preferred width is smaller than the minimum width,
			// recompute the preferred height using the minimum width
			return computeSize(composite, minimumSize.x, hHint, false);
		}
		// If both dimensions are smaller than the minimum size,
		// use the minimum size as our preferred size.
		return minimumSize;
    }

    int[] computeSizes(int[] constraints, int availableSpace,
            boolean computingRows) {
        int[] result = computeMinSizes(constraints, computingRows);

        int totalFixed = sumOfSizes(result);
        int denominator = getResizeDenominator(computingRows);
        int numRows = gridInfo.getNumRows(computingRows);

        if (totalFixed < availableSpace) {
            int remaining = availableSpace - totalFixed;

            for (int idx = 0; idx < numRows && denominator > 0; idx++) {
                Row row = getRow(idx, computingRows);

                if (row.grows) {
                    int greed = row.size;
                    int amount = remaining * greed / denominator;

                    result[idx] += amount;
                    remaining -= amount;
                    denominator -= greed;
                }
            }
        }

        return result;
    }

    /**
     * Computes one dimension of the preferred size of the layout.
     *
     * @param hint contains the result if already known, or SWT.DEFAULT if it needs to be computed
     * @param constraints contains constraints along the other dimension, or SWT.DEFAULT if none. For
     * example, if we are computing the preferred row sizes, this would be an array of known column sizes.
     * @param computingRows if true, this method returns the height (pixels). Otherwise, it returns the
     * width (pixels).
     */
    int preferredSize(int[] constraints, boolean computingRows) {
        int[] fixedSizes = computeMinSizes(constraints, computingRows);

        return sumOfSizes(fixedSizes)
                + getDynamicSize(constraints, fixedSizes, computingRows);
    }

    /**
     * Computes the sum of all integers in the given array. If any of the entries are SWT.DEFAULT,
     * the result is SWT.DEFAULT.
     */
    static int sumOfSizes(int[] input) {
        return sumOfSizes(input, 0, input.length);
    }

    static int sumOfSizes(int[] input, int start, int length) {
        int sum = 0;
        for (int idx = start; idx < start + length; idx++) {
            int next = input[idx];

            if (next == SWT.DEFAULT) {
                return SWT.DEFAULT;
            }

            sum += next;
        }

        return sum;
    }

    /**
     * Returns the preferred dynamic width of the layout
     *
     * @param constraints
     * @param fixedSizes
     * @param computingRows
     * @return
     */
    int getDynamicSize(int[] constraints, int[] fixedSizes,
            boolean computingRows) {
        int result = 0;
        int numerator = getResizeDenominator(computingRows);

        // If no resizable columns, return
        if (numerator == 0) {
            return 0;
        }

        int rowSpacing = computingRows ? verticalSpacing : horizontalSpacing;
        int colSpacing = computingRows ? horizontalSpacing : verticalSpacing;

        int numControls = gridInfo.controls.length;
        for (int idx = 0; idx < numControls; idx++) {
            int controlRowStart = gridInfo.getStartPos(idx, computingRows);
            int controlRowSpan = getSpan(idx, computingRows);
            int controlColStart = gridInfo.getStartPos(idx, !computingRows);
            int controlColSpan = getSpan(idx, !computingRows);

            int denominator = getGrowthRatio(controlRowStart, controlRowSpan,
                    computingRows);

            if (denominator > 0) {

                int widthHint = sumOfSizes(constraints, controlColStart,
                        controlColSpan);
                if (widthHint != SWT.DEFAULT) {
                    widthHint += colSpacing * (controlColSpan - 1);
                }

                // Compute the total control size
                int controlSize = computeControlSize(idx, widthHint,
                        computingRows);

                // Subtract the amount that overlaps fixed-size columns
                controlSize -= sumOfSizes(fixedSizes, controlRowStart,
                        controlRowSpan);

                // Subtract the amount that overlaps spacing between cells
                controlSize -= (rowSpacing * (controlRowSpan - 1));

                result = Math
                        .max(result, controlSize * numerator / denominator);
            }
        }

        return result;
    }

    /**
     * Computes one dimension of a control's size
     *
     * @param control the index of the control being computed
     * @param constraint the other dimension of the control's size, or SWT.DEFAULT if unknown
     * @param computingHeight if true, this method returns a height. Else it returns a width
     * @return the preferred height or width of the control, in pixels
     */
    int computeControlSize(int control, int constraint, boolean computingHeight) {
        CellData data = gridInfo.getCellData(control);

        // If we're looking for the preferred size of the control (without hints)
        if (constraint == SWT.DEFAULT) {
            Point result = data.computeSize(cache.getCache(control),
                    SWT.DEFAULT, SWT.DEFAULT);

            // Return result
            if (computingHeight) {
                return result.y;
            }
            return result.x;
        }

        // Compute a height
        if (computingHeight) {
            return data.computeSize(cache.getCache(control), constraint,
                    SWT.DEFAULT).y;
        }

        return data.computeSize(cache.getCache(control), SWT.DEFAULT,
                constraint).x;
    }

    /**
     * Returns the relative amount that a control starting on the given row and spanning
     * the given length will contribute
     *
     * @param start
     * @param length
     * @param computingRows
     * @return
     */
    int getGrowthRatio(int start, int length, boolean computingRows) {
        boolean willGrow = false;
        int sum = 0;

        int end = start + length;
        for (int idx = start; idx < end; idx++) {
            Row row = getRow(idx, computingRows);

            if (row.largerThanChildren && row.grows) {
                willGrow = true;
            }

            sum += row.size;
        }

        if (!willGrow) {
            return 0;
        }

        return sum;
    }

    int[] computeMinSizes(int[] constraints, boolean computingRows) {
        // We cache the result of this function since it might be called more than once
        // for a single size computation
        int[] result = computingRows ? cachedRowMin : cachedColMin;

        if (result == null) {
            int columnSpacing;
            int rowSpacing;

            if (computingRows) {
                columnSpacing = horizontalSpacing;
                rowSpacing = verticalSpacing;
            } else {
                columnSpacing = verticalSpacing;
                rowSpacing = horizontalSpacing;
            }

            int rowCount = gridInfo.getNumRows(computingRows);
            result = new int[rowCount];
            int colCount = gridInfo.getNumRows(!computingRows);
            int[] rowControls = new int[colCount];

            int lastGrowingRow = -1;

            for (int idx = 0; idx < rowCount; idx++) {
                Row row = getRow(idx, computingRows);

                if (row.grows) {
                    // There is no minimum size for growing rows
                    lastGrowingRow = idx;
                    result[idx] = 0;
                } else {
                    result[idx] = row.size;

                    if (row.largerThanChildren) {
                        // Determine which controls are in this row
                        gridInfo.getRow(rowControls, idx, computingRows);

                        for (int control : rowControls) {
                            // The getRow method will insert -1 into empty cells... skip these.
                            if (control != -1) {
                                int controlStart = gridInfo.getStartPos(
                                        control, computingRows);
                                int controlSpan = getSpan(control,
                                        computingRows);

                                // If the control ends on this row and does not span any growing rows
                                if (controlStart + controlSpan - 1 == idx
                                        && controlStart > lastGrowingRow) {
                                    int controlColStart = gridInfo.getStartPos(
                                            control, !computingRows);
                                    int controlColSpan = getSpan(control,
                                            !computingRows);
                                    int controlRowSpan = getSpan(control,
                                            computingRows);

                                    // Compute the width constraint for this control
                                    int spannedWidth = sumOfSizes(constraints,
                                            controlColStart, controlColSpan);
                                    if (spannedWidth != SWT.DEFAULT) {
                                        spannedWidth += (columnSpacing * (controlSpan - 1));
                                    }

                                    int controlHeight = computeControlSize(
                                            control, spannedWidth,
                                            computingRows);

                                    // Determine how much of the control spans already allocated columns
                                    int allocatedHeight = sumOfSizes(result,
                                            controlColStart, controlRowSpan - 1)
                                            + (rowSpacing * (controlRowSpan - 1));

                                    result[idx] = Math.max(result[idx],
                                            controlHeight - allocatedHeight);
                                }
                            }
                        }
                    }
                }
            }
        }

        // Cache this result
        if (computingRows) {
            cachedRowMin = result;
        } else {
            cachedColMin = result;
        }

        return result;
    }

    /**
     * Returns the height constraints that should be used when computing column widths. Requires initGrid
     * to have been called first.
     *
     * @param result Will contain the height constraint for row i in the ith position of the array,
     * or SWT.DEFAULT if there is no constraint on that row.
     */
    private int[] computeConstraints(boolean horizontal) {
        // Initialize the height constraints for each row (basically, these will always be SWT.DEFAULT,
        // except for rows of type FixedRow, which have a constant height).
        int numRows = gridInfo.getNumRows(horizontal);
        int[] result = new int[numRows];

        for (int idx = 0; idx < numRows; idx++) {
            Row row = getRow(idx, horizontal);

            if (!(row.grows || row.largerThanChildren)) {
                result[idx] = row.size;
            } else {
                result[idx] = SWT.DEFAULT;
            }
        }

        return result;
    }

    /**
     * Computes the total greediness of all rows
     *
     * @return the total greediness of all rows
     */
    private int getResizeDenominator(boolean horizontal) {
        int result = 0;
        int numRows = gridInfo.getNumRows(horizontal);

        for (int idx = 0; idx < numRows; idx++) {
            Row row = getRow(idx, horizontal);

            if (row.grows) {
                result += row.size;
            }
        }

        return result;
    }

    //	/**
    //	 * Computes the total fixed height of all rows
    //	 *
    //	 * @param widthConstraints array where the nth entry indicates the known width of the
    //	 * nth column, or SWT.DEFAULT if the width is still unknown
    //	 *
    //	 * @return the total fixed height for all rows
    //	 */
    //	private int getMinimumSize(int[] constraints, boolean horizontal) {
    //		Control[] controls = new Control[gridInfo.getRows()];
    //		int result = 0;
    //		int numRows = gridInfo.getRows();
    //
    //		for (int idx = 0; idx < numRows; idx++) {
    //			result += getRow(idx).getFixedHeight(gridInfo, widthConstraints, idx);
    //		}
    //
    //		return result;
    //	}

    protected int getSpan(int controlId, boolean isRow) {
        CellData data = gridInfo.getCellData(controlId);

        if (isRow) {
            return data.verticalSpan;
        }
        return data.horizontalSpan;
    }

    /**
     * Returns the total space that will be required for margins and spacing between and
     * around cells. initGrid(...) must have been called first.
     *
     * @return
     */
    private Point totalEmptySpace() {
        int numRows = gridInfo.getRows();

        return new Point((2 * marginWidth)
                + ((gridInfo.getCols() - 1) * horizontalSpacing),
                (2 * marginHeight) + ((numRows - 1) * verticalSpacing));
    }

    /**
     * Returns the absolute positions of each row, given the start position, row sizes,
     * and row spacing
     *
     * @param startPos position of the initial row
     * @param sizes array of row sizes (pixels)
     * @param spacing space between each row (pixels)
     * @return array of row positions. The result size is sizes.length + 1. The last entry is
     * the position of the end of the layout.
     */
    private static int[] computeRowPositions(int startPos, int[] sizes,
            int spacing) {
        int[] result = new int[sizes.length + 1];

        result[0] = startPos;
        for (int idx = 0; idx < sizes.length; idx++) {
            result[idx + 1] = result[idx] + sizes[idx] + spacing;
        }

        return result;
    }

    @Override
	protected void layout(Composite composite, boolean flushCache) {
        Control[] children = composite.getChildren();

        // If there are no children then this is a NO-OP
        if (children.length == 0)
        	return;

        initGrid(children);

        if (flushCache) {
            cache.flush();
        }

        Point emptySpace = totalEmptySpace();

        // Compute the area actually available for controls (once the margins and spacing is removed)
        int availableWidth = composite.getClientArea().width - emptySpace.x;
        int availableHeight = composite.getClientArea().height - emptySpace.y;

        int[] heights = computeConstraints(true);
        int[] widths = new int[gridInfo.getCols()];

        // Compute the actual column widths
        widths = computeSizes(heights, availableWidth, false);

        // Compute the actual row heights (based on the actual column widths)
        heights = computeSizes(widths, availableHeight, true);

        Rectangle currentCell = new Rectangle(0, 0, 0, 0);

        int[] starty = computeRowPositions(composite.getClientArea().y
                + marginHeight, heights, verticalSpacing);
        int[] startx = computeRowPositions(composite.getClientArea().x
                + marginWidth, widths, horizontalSpacing);

        int numChildren = gridInfo.controls.length;
        for (int controlId = 0; controlId < numChildren; controlId++) {
            CellData data = gridInfo.getCellData(controlId);

            int row = gridInfo.controlRow[controlId];
            int col = gridInfo.controlCol[controlId];

            currentCell.x = startx[col];
            currentCell.width = startx[col + data.horizontalSpan]
                    - currentCell.x - horizontalSpacing;

            currentCell.y = starty[row];
            currentCell.height = starty[row + data.verticalSpan]
                    - currentCell.y - verticalSpacing;

            data.positionControl(cache.getCache(controlId), currentCell);
        }
    }

    /**
     * @return
     */
    public int getColumns() {
        return numCols;
    }

    public boolean canGrow(Composite composite, boolean horizontally) {
        initGrid(composite.getChildren());

        int numRows = gridInfo.getNumRows(horizontally);

        for (int idx = 0; idx < numRows; idx++) {
            Row row = getRow(idx, horizontally);

            if (row.grows) {
                return true;
            }
        }

        return false;

    }
}
