/*******************************************************************************
 * 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<Row> 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<Row> 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 horizontalSpacing the number of pixels of empty space between adjacent columns
	 * @param verticalSpacing the number of pixels of empty space between adjacent rows
	 */
	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(int, Row)
	 */
	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 = rows.get(rowNum);

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

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

		Row result = 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);

		// Compute the actual column widths
		int[] 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;

	}
}
