/*******************************************************************************
 * 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 java.util.List;
import org.eclipse.jface.util.Geometry;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.ProgressBar;
import org.eclipse.swt.widgets.Sash;
import org.eclipse.swt.widgets.Scale;
import org.eclipse.swt.widgets.Slider;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.Tree;

/**
 * Caches the preferred size of an SWT control
 *
 * @since 3.0
 */
public class SizeCache {
	private Control control;

	private Point preferredSize;

	private Point cachedWidth;

	private Point cachedHeight;

	/**
	 * True iff we should recursively flush all children on the next layout
	 */
	private boolean flushChildren;

	/**
	 * True iff changing the height hint does not affect the preferred width and
	 * changing the width hint does not change the preferred height
	 */
	private boolean independentDimensions = false;

	/**
	 * True iff the preferred height for any hint larger than the preferred width
	 * will not change the preferred height.
	 */
	private boolean preferredWidthOrLargerIsMinimumHeight = false;

	// HACK: these values estimate how much to subtract from the width and height
	// hints that get passed into computeSize, in order to produce a result
	// that is exactly the desired size. To be removed once bug 46112 is fixed
	// (note:
	// bug 46112 is currently flagged as a duplicate, but there is still no
	// workaround).
	private int widthAdjustment = 0;

	private int heightAdjustment = 0;

	// END OF HACK

	public SizeCache() {
		this(null);
	}

	/**
	 * Creates a cache for size computations on the given control
	 *
	 * @param control the control for which sizes will be calculated, or null to
	 *                always return (0,0)
	 */
	public SizeCache(Control control) {
		setControl(control);
	}

	/**
	 * Sets the control whose size is being cached. Does nothing (will not even
	 * flush the cache) if this is the same control as last time or it is already
	 * disposed.
	 *
	 * @param newControl the control whose size is being cached, or null to always
	 *                   return (0,0)
	 */
	public void setControl(Control newControl) {
		if (newControl != control) {
			control = newControl;
			if (control == null) {
				independentDimensions = true;
				preferredWidthOrLargerIsMinimumHeight = false;
				widthAdjustment = 0;
				heightAdjustment = 0;
			} else if (!control.isDisposed()) {
				independentDimensions = independentLengthAndWidth(control);
				preferredWidthOrLargerIsMinimumHeight = isPreferredWidthMaximum(control);
				computeHintOffset(control);
				flush();
			}
		}
	}

	/**
	 * Returns the control whose size is being cached
	 *
	 * @return the control whose size is being cached, or null if this cache always
	 *         returns (0,0)
	 */
	public Control getControl() {
		return control;
	}

	/**
	 * Flush the cache (should be called if the control's contents may have changed
	 * since the last query)
	 */
	public void flush() {
		flush(true);
	}

	public void flush(boolean recursive) {
		preferredSize = null;
		cachedWidth = null;
		cachedHeight = null;
		this.flushChildren = recursive;
	}

	private Point getPreferredSize() {
		if (preferredSize == null) {
			preferredSize = computeSize(control, SWT.DEFAULT, SWT.DEFAULT);
		}

		return preferredSize;
	}

	/**
	 * Computes the preferred size of the control.
	 *
	 * @param widthHint  the known width of the control (pixels) or SWT.DEFAULT if
	 *                   unknown
	 * @param heightHint the known height of the control (pixels) or SWT.DEFAULT if
	 *                   unknown
	 * @return the preferred size of the control
	 */
	public Point computeSize(int widthHint, int heightHint) {
		if (control == null) {
			return new Point(0, 0);
		}

		// If both dimensions were supplied in the input, return them verbatim
		if (widthHint != SWT.DEFAULT && heightHint != SWT.DEFAULT) {
			return new Point(widthHint, heightHint);
		}

		// No hints given -- find the preferred size
		if (widthHint == SWT.DEFAULT && heightHint == SWT.DEFAULT) {
			return Geometry.copy(getPreferredSize());
		}

		// If the length and width are independent, compute the preferred size
		// and adjust whatever dimension was supplied in the input
		if (independentDimensions) {
			Point result = Geometry.copy(getPreferredSize());

			if (widthHint != SWT.DEFAULT) {
				result.x = widthHint;
			}

			if (heightHint != SWT.DEFAULT) {
				result.y = heightHint;
			}

			return result;
		}

		// Computing a height
		if (heightHint == SWT.DEFAULT) {
			// If we know the control's preferred size
			if (preferredSize != null) {
				// If the given width is the preferred width, then return the preferred size
				if (widthHint == preferredSize.x) {
					return Geometry.copy(preferredSize);
				}
			}

			// If we have a cached height measurement
			if (cachedHeight != null) {
				// If this was measured with the same width hint
				if (cachedHeight.x == widthHint) {
					return Geometry.copy(cachedHeight);
				}
			}

			// If this is a control where any hint larger than the
			// preferred width results in the minimum height, determine if
			// we can compute the result based on the preferred height
			if (preferredWidthOrLargerIsMinimumHeight) {
				// Computed the preferred size (if we don't already know it)
				getPreferredSize();

				// If the width hint is larger than the preferred width, then
				// we can compute the result from the preferred width
				if (widthHint >= preferredSize.x) {
					Point result = Geometry.copy(preferredSize);
					result.x = widthHint;
					return result;
				}
			}

			// Else we can't find an existing size in the cache, so recompute
			// it from scratch.
			cachedHeight = computeSize(control, widthHint, heightHint);

			return Geometry.copy(cachedHeight);
		}

		// Computing a width
		if (widthHint == SWT.DEFAULT) {
			// If we know the control's preferred size
			if (preferredSize != null) {
				// If the given height is the preferred height, then return the preferred size
				if (heightHint == preferredSize.y) {
					return Geometry.copy(preferredSize);
				}
			}

			// If we have a cached width measurement
			if (cachedWidth != null) {
				// If this was measured with the same height hint
				if (cachedWidth.y == heightHint) {
					return Geometry.copy(cachedWidth);
				}
			}

			cachedWidth = computeSize(control, widthHint, heightHint);

			return Geometry.copy(cachedWidth);
		}

		return computeSize(control, widthHint, heightHint);
	}

	/**
	 * Compute the control's size, and ensure that non-default hints are returned
	 * verbatim (this tries to compensate for SWT's hints, which aren't really the
	 * outer width of the control).
	 *
	 * @param control
	 * @param widthHint
	 * @param heightHint
	 * @return
	 */
	private Point computeSize(Control control, int widthHint, int heightHint) {
		int adjustedWidthHint = widthHint == SWT.DEFAULT ? SWT.DEFAULT : Math.max(0, widthHint - widthAdjustment);
		int adjustedHeightHint = heightHint == SWT.DEFAULT ? SWT.DEFAULT : Math.max(0, heightHint - heightAdjustment);

		Point result = control.computeSize(adjustedWidthHint, adjustedHeightHint, flushChildren);
		flushChildren = false;

		// If the amounts we subtracted off the widthHint and heightHint didn't do the
		// trick, then
		// manually adjust the result to ensure that a non-default hint will return that
		// result verbatim.

		if (widthHint != SWT.DEFAULT) {
			result.x = widthHint;
		}

		if (heightHint != SWT.DEFAULT) {
			result.y = heightHint;
		}

		return result;
	}

	/**
	 * Returns true if the preferred length of the given control is independent of
	 * the width and visa-versa. If this returns true, then changing the widthHint
	 * argument to control.computeSize will never change the resulting height and
	 * changing the heightHint will never change the resulting width. Returns false
	 * if unknown.
	 * <p>
	 * This information can be used to improve caching. Incorrectly returning a
	 * value of false may decrease performance, but incorrectly returning a value of
	 * true will generate incorrect layouts... so always return false if unsure.
	 * </p>
	 *
	 * @param control
	 * @return
	 */
	static boolean independentLengthAndWidth(Control control) {
		if (control == null) {
			return true;
		}

		if (control instanceof Button || control instanceof ProgressBar || control instanceof Sash
				|| control instanceof Scale || control instanceof Slider || control instanceof List
				|| control instanceof Combo || control instanceof Tree) {
			return true;
		}

		if (control instanceof Label || control instanceof Text) {
			return (control.getStyle() & SWT.WRAP) == 0;
		}

		// Unless we're certain that the control has this property, we should
		// return false.

		return false;
	}

	/**
	 * Try to figure out how much we need to subtract from the hints that we pass
	 * into the given control's computeSize(...) method. This tries to compensate
	 * for bug 46112. To be removed once SWT provides an "official" way to compute
	 * one dimension of a control's size given the other known dimension.
	 *
	 * @param control
	 */
	private void computeHintOffset(Control control) {
		if (control instanceof Composite) {
			// For composites, subtract off the trim size
			Composite composite = (Composite) control;
			Rectangle trim = composite.computeTrim(0, 0, 0, 0);

			widthAdjustment = trim.width;
			heightAdjustment = trim.height;
		} else {
			// For non-composites, subtract off 2 * the border size
			widthAdjustment = control.getBorderWidth() * 2;
			heightAdjustment = widthAdjustment;
		}
	}

	/**
	 * Returns true only if the control will return a constant height for any width
	 * hint larger than the preferred width. Returns false if there is any situation
	 * in which the control does not have this property.
	 *
	 * <p>
	 * Note: this method is only important for wrapping controls, and it can safely
	 * return false for anything else. AFAIK, all SWT controls have this property,
	 * but to be safe they will only be added to the list once the property has been
	 * confirmed.
	 * </p>
	 *
	 * @param control
	 * @return
	 */
	private static boolean isPreferredWidthMaximum(Control control) {
		return (control instanceof ToolBar
				// || control instanceof CoolBar
				|| control instanceof Label);
	}

}
