/*******************************************************************************
 * Copyright (c) 2004, 2015 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 org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;

/**
 * <code>CellData</code> is the layout data object associated with
 * <code>CellLayout</code>. You can attach a CellData object to a control by
 * using the <code>setLayoutData</code> method. CellData objects are optional.
 * If you do not attach any layout data to a control, it will behave just like
 * attaching a CellData created using its default constructor.
 *
 * @since 3.0
 **/
public final class CellData {

	/**
	 * hintType flag (value = 0) indicating that the control's computeSize method
	 * should be used to determine the control size. If modifierType is set to NONE,
	 * then the widthHint and heightHint fields will be ignored.
	 */
	public static final int NONE = 0;

	/**
	 * hintType flag (value = 1) indicating that the widthHint and heightHint should
	 * be used as the control's size instead of the result of computeSize
	 * <p>
	 * This flag is useful for list boxes, text boxes, tree controls, and other
	 * controls whose contents can change dynamically. For example, create a tree
	 * control and set its width and height hints to the default size for that
	 * control. This will cause the hints to be used instead of the preferred size
	 * of the tree control.
	 * </p>
	 */
	public static final int OVERRIDE = 1;

	/**
	 * hintType(value = 2) indicating that the width of the control should be no
	 * less than widthHint (if provided) and the height of the control should be no
	 * less than heightHint (if provided).
	 * <p>
	 * This flag is useful for buttons. For example, set the width and height hints
	 * to the default button size. This will use the default button size unless the
	 * button label is too large to fit on the button.
	 * </p>
	 */
	public static final int MINIMUM = 2;

	/**
	 * hintType flag (value = 3) indicating that the width of the control should be
	 * no more than widthHint (if provided) and the height of the control should be
	 * no more than heightHint (if provided).
	 * <p>
	 * This flag is useful for wrapping text. For example, set heightHint to
	 * SWT.DEFAULT and set widthHint to the desired number of pixels after which
	 * text should wrap. This will cause the text to wrap after the given number of
	 * pixels, but will not allocate extra space in the column if the text widget
	 * does not fill an entire line.
	 * </p>
	 */
	public static final int MAXIMUM = 3;

	/**
	 * This flag controls how the width and height hints are to be treated. See the
	 * constants above.
	 */
	public int hintType = OVERRIDE;

	/**
	 * Width hint. This modifies the width of the control, in pixels. If set to
	 * SWT.DEFAULT, this dimension will not be constrained. Depending on the value
	 * of modifierType, this may be a minimum size, a maximum size, or simply
	 * replace the preferred control size.
	 */
	public int widthHint = SWT.DEFAULT;

	/**
	 * Height hint. This modifies the height of the control, in pixels. If set to
	 * SWT.DEFAULT, this dimension will not be constrained. Depending on the value
	 * of modifierType, this will be a minimum size, a maximum size, or a
	 * replacement for the control's preferred size.
	 */
	public int heightHint = SWT.DEFAULT;

	/**
	 * Number of rows spanned by this cell (default = 1)
	 */
	public int verticalSpan = 1;

	/**
	 * Number of columns spanned by this cell (default = 1)
	 */
	public int horizontalSpan = 1;

	/**
	 * Horizontal alignment of the control within the cell. May be one of SWT.LEFT,
	 * SWT.RIGHT, SWT.CENTER, or SWT.NORMAL. SWT.NORMAL indicates that the control
	 * should be made as wide as the cell.
	 */
	public int horizontalAlignment = SWT.FILL;

	/**
	 * Vertical alignment of the control within the cell. May be one of SWT.TOP,
	 * SWT.BOTTOM, SWT.CENTER, or SWT.NORMAL. SWT.NORMAL indicates that the control
	 * should be made as wide as the cell.
	 */
	public int verticalAlignment = SWT.FILL;

	/**
	 * Horizontal indentation (pixels). Positive values move the control to the
	 * right, negative to the left.
	 */
	public int horizontalIndent = 0;

	/**
	 * Vertical indentation (pixels). Positive values move the control down,
	 * negative values move the control up.
	 */
	public int verticalIndent = 0;

	/**
	 * Constructs a CellData with default properties
	 */
	public CellData() {
		// Use the default values for all fields.
	}

	/**
	 * Creates a new CellData that with properties that are as close as possible to
	 * the given GridData. This is used for converting GridLayouts into CellLayouts.
	 *
	 * @param data
	 */
	public CellData(GridData data) {
		verticalSpan = data.verticalSpan;
		horizontalSpan = data.horizontalSpan;

		switch (data.horizontalAlignment) {
		case GridData.BEGINNING:
			horizontalAlignment = SWT.LEFT;
			break;
		case GridData.CENTER:
			horizontalAlignment = SWT.CENTER;
			break;
		case GridData.END:
			horizontalAlignment = SWT.RIGHT;
			break;
		case GridData.FILL:
			horizontalAlignment = SWT.FILL;
			break;
		}

		switch (data.verticalAlignment) {
		case GridData.BEGINNING:
			verticalAlignment = SWT.LEFT;
			break;
		case GridData.CENTER:
			verticalAlignment = SWT.CENTER;
			break;
		case GridData.END:
			verticalAlignment = SWT.RIGHT;
			break;
		case GridData.FILL:
			verticalAlignment = SWT.FILL;
			break;
		}

		widthHint = data.widthHint;
		heightHint = data.heightHint;
		horizontalIndent = data.horizontalIndent;
		hintType = OVERRIDE;
	}

	/**
	 * Copies the given CellData
	 *
	 * @param newData
	 */
	public CellData(CellData newData) {
		hintType = newData.hintType;
		widthHint = newData.widthHint;
		heightHint = newData.heightHint;
		horizontalAlignment = newData.horizontalAlignment;
		verticalAlignment = newData.verticalAlignment;
		horizontalSpan = newData.horizontalSpan;
		verticalSpan = newData.verticalSpan;
	}

	/**
	 * Sets the size hint for this control. This is used to modify the control's
	 * preferred size. If one dimension should remain unmodified, that hint can be
	 * set to SWT.DEFAULT. Using a size hint of CellData.MINIMUM ensures that the
	 * preferred control size is larger than the hint. Using a size hint of
	 * CellData.MAXIMUM ensures that the preferred size is smaller than the hint.
	 * Using a size hint of CellData.OVERRIDE ensures that the preferred size is
	 * always equal to the hint.
	 *
	 * @param hintType one of CellData.MINIMUM, CellData.MAXIMUM, or
	 *                 CellData.OVERRIDE
	 * @param hint     size hint (in pixels). If either dimension is set to
	 *                 SWT.DEFAULT, the hint will not affect that dimension
	 * @return this
	 */
	public CellData setHint(int hintType, Point hint) {
		return setHint(hintType, hint.x, hint.y);
	}

	/**
	 * Sets the size hint for this control. This is used to modify the control's
	 * preferred size. If one dimension should remain unmodified, that hint can be
	 * set to SWT.DEFAULT. Using a size hint of CellData.MINIMUM ensures that the
	 * preferred control size is larger than the hint. Using a size hint of
	 * CellData.MAXIMUM ensures that the preferred size is smaller than the hint.
	 * Using a size hint of CellData.OVERRIDE ensures that the preferred size is
	 * always equal to the hint. If both hints are equal to SWT.DEFAULT, then the
	 * control's preferred size is unmodified.
	 *
	 * @param hintType   one of CellData.MINIMUM, CellData.MAXIMUM, or
	 *                   CellData.OVERRIDE
	 * @param horizontal horizontal hint (pixels). A value of SWT.DEFAULT will leave
	 *                   the result of the control's computeSize method unmodified.
	 * @param vertical   vertical hint (pixels). A value of SWT.DEFAULT will leave
	 *                   the result of the control's computeSize method unmodified.
	 * @return this
	 */
	public CellData setHint(int hintType, int horizontal, int vertical) {
		this.hintType = hintType;
		this.heightHint = vertical;
		this.widthHint = horizontal;

		return this;
	}

	/**
	 * Sets the alignment for this control
	 *
	 * @param horizontalAlignment one of SWT.LEFT, SWT.RIGHT, SWT.FILL, or
	 *                            SWT.CENTER
	 * @param verticalAlignment   one of SWT.TOP, SWT.BOTTOM, SWT.FILL, or
	 *                            SWT.CENTER
	 * @return this
	 */
	public CellData align(int horizontalAlignment, int verticalAlignment) {
		this.horizontalAlignment = horizontalAlignment;
		this.verticalAlignment = verticalAlignment;

		return this;
	}

	/**
	 * Sets the number of rows and columns spanned by this control.
	 *
	 * @param horizontalSpan number of columns spanned by the control (&gt; 0)
	 * @param verticalSpan   number of rows spanned by the control (&gt; 0)
	 * @return this
	 */
	public CellData span(int horizontalSpan, int verticalSpan) {
		this.horizontalSpan = horizontalSpan;
		this.verticalSpan = verticalSpan;

		return this;
	}

	/**
	 * Sets the indentation for this control. The indentation is added to the
	 * control's position within the cell. For example, indentation of (10,4) will
	 * move the control right by 10 pixels and down by 4 pixels.
	 *
	 * @param indent indentation (pixels)
	 * @return this
	 */
	public CellData indent(Point indent) {
		return this.indent(indent.x, indent.y);
	}

	/**
	 * Sets the indentation for this cell
	 *
	 * @param horizontalIndent distance (pixels) to move the control to the right
	 * @param verticalIndent   distance (pixels) to move the control down
	 * @return this
	 */
	public CellData indent(int horizontalIndent, int verticalIndent) {
		this.horizontalIndent = horizontalIndent;
		this.verticalIndent = verticalIndent;

		return this;
	}

	/**
	 * Returns the preferred size of the given control, given the known dimensions
	 * of its cell.
	 *
	 * @param toCompute  the control whose size is to be computed
	 * @param cellWidth  width of the cell, in pixels (or SWT.DEFAULT if unknown)
	 * @param cellHeight height of the cell, in pixels (or SWT.DEFAULT if unknown)
	 * @return the preferred size of the given control, in pixels
	 */
	public Point computeSize(SizeCache toCompute, int cellWidth, int cellHeight) {

		int absHorizontalIndent = Math.abs(horizontalIndent);
		int absVerticalIndent = Math.abs(verticalIndent);

		// If we're going to indent, subtract off the space that will be required for
		// indentation from
		// the available space
		if (cellWidth != SWT.DEFAULT) {
			cellWidth -= absHorizontalIndent;
		}

		if (cellHeight != SWT.DEFAULT) {
			cellHeight -= absVerticalIndent;
		}

		int controlWidth = horizontalAlignment == SWT.FILL ? cellWidth : SWT.DEFAULT;
		int controlHeight = verticalAlignment == SWT.FILL ? cellHeight : SWT.DEFAULT;

		// Note: this could be optimized further. If we're using a MAXIMUM hint and
		// non-FILL alignment, we could simply call computeMaximumBoundedSize using the
		// minimum of the cell size and the hint as the boundary -- basically, rather
		// than applying two limits for the hint and the cell boundary, we can do it in
		// one step and reduce the size computations by half (for this specific case).
		Point controlSize = computeControlSize(toCompute, controlWidth, controlHeight);

		if (cellWidth != SWT.DEFAULT && controlSize.x > cellWidth) {
			controlSize = computeControlSize(toCompute, cellWidth, controlHeight);
			if (cellHeight != SWT.DEFAULT && controlSize.y > cellHeight) {
				controlSize.y = cellHeight;
			}
		} else if (cellHeight != SWT.DEFAULT && controlSize.y > cellHeight) {
			controlSize = computeControlSize(toCompute, controlWidth, cellHeight);
			if (cellWidth != SWT.DEFAULT && controlSize.x > cellWidth) {
				controlSize.x = cellWidth;
			}
		}

		// If we're going to indent, add the indentation to the required space
		controlSize.x += absHorizontalIndent;
		controlSize.y += absVerticalIndent;

		return controlSize;
	}

	/**
	 * Arranges the given control within the given rectangle using the criteria
	 * described by this CellData.
	 *
	 * @param cache
	 * @param cellBounds
	 * @since 3.0
	 */
	public void positionControl(SizeCache cache, Rectangle cellBounds) {

		int startx = cellBounds.x;
		int starty = cellBounds.y;
		int availableWidth = cellBounds.width - horizontalIndent;
		int availableHeight = cellBounds.height - verticalIndent;

		Point size = computeSize(cache, availableWidth, availableHeight);

		// Horizontal justification
		switch (horizontalAlignment) {
		case SWT.RIGHT:
			startx = cellBounds.x + availableWidth - size.x;
			break;
		case SWT.CENTER:
			startx = cellBounds.x + (availableWidth - size.x) / 2;
			break;
		}

		// Vertical justification
		switch (verticalAlignment) {
		case SWT.BOTTOM:
			starty = cellBounds.y + availableHeight - size.y;
			break;
		case SWT.CENTER:
			starty = cellBounds.y + (availableHeight - size.y) / 2;
			break;
		}

		// Position the control
		cache.getControl().setBounds(startx + horizontalIndent, starty + verticalIndent, size.x, size.y);
	}

	/**
	 * Returns the preferred size of the given control in this cell, given one or
	 * both known dimensions of the control. This differs from computeSize, which
	 * takes known dimensions of the <b>cell</b> as arguments.
	 *
	 * @param toCompute
	 * @param controlWidth
	 * @param controlHeight
	 * @return
	 * @since 3.0
	 */
	private Point computeControlSize(SizeCache toCompute, int controlWidth, int controlHeight) {
		switch (hintType) {
		case OVERRIDE:
			return computeOverrideSize(toCompute, controlWidth, controlHeight, widthHint, heightHint);
		case MINIMUM:
			return computeMinimumBoundedSize(toCompute, controlWidth, controlHeight, widthHint, heightHint);
		case MAXIMUM:
			return computeMaximumBoundedSize(toCompute, controlWidth, controlHeight, widthHint, heightHint);
		}

		return computeRawSize(toCompute, controlWidth, controlHeight);
	}

	/**
	 * Computes the size of the control, given its outer dimensions. This should be
	 * used in place of calling Control.computeSize, since Control.computeSize takes
	 * control-specific inner dimensions as hints.
	 *
	 * @param toCompute     Control whose size will be computed
	 * @param controlWidth  width of the control (pixels or SWT.DEFAULT if unknown)
	 * @param controlHeight height of the control (pixels or SWT.DEFAULT if unknown)
	 * @return preferred dimensions of the control
	 */
	private static Point computeRawSize(SizeCache toCompute, int controlWidth, int controlHeight) {
		if (controlWidth != SWT.DEFAULT && controlHeight != SWT.DEFAULT) {
			return new Point(controlWidth, controlHeight);
		}

		// Known bug: we pass the OUTER dimension of the control into computeSize, even
		// though
		// SWT expects a control-specific inner dimension as width and height hints.
		// Currently,
		// SWT does not provide any means to convert outer dimensions into inner
		// dimensions.
		// Fortunately, the outer and inner dimensions tend to be quite close so we
		// pass in the outer dimension and adjust the result if it differs from one of
		// the
		// hints. This may cause incorrect text wrapping in rare cases, and should be
		// fixed
		// once SWT provides a way to convert the outer dimension of a control into a
		// valid
		// width or height hint for Control.computeSize. Note that the distinction
		// between outer
		// and inner dimensions is undocumented in SWT, and most examples also contain
		// this
		// bug.
		Point result = toCompute.computeSize(controlWidth, controlHeight);

		// Hack: If the result of computeSize differs from the width or height-hints,
		// adjust it.
		// See above. Don't remove this hack until SWT provides some way to pass correct
		// width
		// and height hints into computeSize. Once this happens, these conditions should
		// always
		// return false and the hack will have no effect.
		if (controlWidth != SWT.DEFAULT) {
			result.x = controlWidth;
		} else if (controlHeight != SWT.DEFAULT) {
			result.y = controlHeight;
		}

		return result;
	}

	/**
	 * Computes the preferred size of the control. Optionally, one or both
	 * dimensions may be fixed to a given size.
	 *
	 * @param control   object that can compute the size of the control of interest
	 * @param wHint     known width (or SWT.DEFAULT if the width needs to be
	 *                  computed)
	 * @param hHint     known height (or SWT.DEFAULT if the height needs to be
	 *                  computed)
	 * @param overrideW width that should always be returned by the control, or
	 *                  SWT.DEFAULT if the width is not being constrained
	 * @param overrideH height that should always be returned by the control, or
	 *                  SWT.DEFAULT if the height is not being constrained
	 * @return
	 */
	private static Point computeOverrideSize(SizeCache control, int wHint, int hHint, int overrideW, int overrideH) {
		int resultWidth = overrideW;
		int resultHeight = overrideH;

		if (wHint != SWT.DEFAULT) {
			resultWidth = wHint;
		}

		if (hHint != SWT.DEFAULT) {
			resultHeight = hHint;
		}

		if (resultWidth == SWT.DEFAULT || resultHeight == SWT.DEFAULT) {
			return computeRawSize(control, resultWidth, resultHeight);
		}

		return new Point(resultWidth, resultHeight);
	}

	/**
	 * Computes the size for the control, optionally bounding the size in the x and
	 * y directions. The various hints are used to determine which dimensions are
	 * already known and which dimensions need to be computed.
	 *
	 * @param control       The control whose size should be computed
	 * @param wHint         known width (or SWT.DEFAULT if the width needs to be
	 *                      computed)
	 * @param hHint         known height (or SWT.DEFAULT if the height needs to be
	 *                      computed)
	 * @param boundedWidth  maximum width for the control (or SWT.DEFAULT if the
	 *                      width is unbounded)
	 * @param boundedHeight maximum height for the control (or SWT.DEFAULT if the
	 *                      height is unbounded)
	 * @return the preferred size of the control, given that it cannot exceed the
	 *         given bounds
	 */
	private static Point computeMaximumBoundedSize(SizeCache control, int wHint, int hHint, int boundedWidth,
			int boundedHeight) {
		Point controlSize = computeRawSize(control, wHint, hHint);

		if (wHint == SWT.DEFAULT && boundedWidth != SWT.DEFAULT && controlSize.x > boundedWidth) {
			return computeMaximumBoundedSize(control, boundedWidth, hHint, boundedWidth, boundedHeight);
		}

		if (hHint == SWT.DEFAULT && boundedHeight != SWT.DEFAULT && controlSize.y > boundedHeight) {
			return computeMaximumBoundedSize(control, wHint, boundedHeight, boundedWidth, boundedHeight);
		}

		return controlSize;
	}

	private static Point computeMinimumBoundedSize(SizeCache control, int wHint, int hHint, int minimumWidth,
			int minimumHeight) {

		Point controlSize = computeRawSize(control, wHint, hHint);

		if (minimumWidth != SWT.DEFAULT && wHint == SWT.DEFAULT && controlSize.x < minimumWidth) {
			return computeMinimumBoundedSize(control, minimumWidth, hHint, minimumWidth, minimumHeight);
		}

		if (minimumHeight != SWT.DEFAULT && hHint == SWT.DEFAULT && controlSize.y < minimumHeight) {
			return computeMinimumBoundedSize(control, wHint, minimumHeight, minimumWidth, minimumHeight);
		}

		return controlSize;
	}

}
