/*******************************************************************************
 * 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 && !newControl.isDisposed()) {
            control = newControl;
            if (control == null) {
                independentDimensions = true;
                preferredWidthOrLargerIsMinimumHeight = false;
                widthAdjustment = 0;
                heightAdjustment = 0;
            } else {
                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);
    }

}
