/*******************************************************************************
 * 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 (> 0)
     * @param verticalSpan number of rows spanned by the control (> 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 control
     * @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) {
            Point result = computeRawSize(control, resultWidth, resultHeight);

            return result;
        }

        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;
    }

}
