/*******************************************************************************
 * Copyright (c) 2004, 2007 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.ui.forms.widgets;

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.Layout;
import org.eclipse.swt.widgets.ProgressBar;
import org.eclipse.swt.widgets.Sash;
import org.eclipse.swt.widgets.Scale;
import org.eclipse.swt.widgets.Scrollable;
import org.eclipse.swt.widgets.Slider;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.ui.internal.forms.widgets.FormUtil;

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

    private Point preferredSize;

    private int cachedWidthQuery;
    private int cachedWidthResult;

    private int cachedHeightQuery;
    private int cachedHeightResult;
    
    private int minimumWidth;
    private int heightAtMinimumWidth = -1;
    private int maximumWidth;
    
    /**
     * 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;

    private int minimumHeight;

    private int widthAtMinimumHeight = -1;
    
    // If the layout is dirty, this is the size of the control at the time its
    // layout was dirtied. null if the layout is not dirty.
    private Point dirtySize = null; 


    // 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. 
     * 
     * @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 {
                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;
        cachedWidthQuery = -1;
        cachedWidthResult = -1;
        cachedHeightQuery = -1;
        cachedHeightResult = -1;
        minimumWidth = -1;
        maximumWidth = -1;
        minimumHeight = -1;
        heightAtMinimumWidth = -1;
        widthAtMinimumHeight = -1;
        
        if (recursive || dirtySize != null) {
            if (control == null || control.isDisposed()) {
                dirtySize = new Point(0,0);
                control = null;
            } else {
                dirtySize = control.getSize();
            }
        }
        
        this.flushChildren = this.flushChildren || recursive;
    }

    private Point getPreferredSize() {
        if (preferredSize == null) {
            preferredSize = controlComputeSize(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 || control.isDisposed()) {
            return new Point(0, 0);
        }
        
        // If we're asking for a result smaller than the minimum width
        int minWidth = computeMinimumWidth();
        
        if (widthHint != SWT.DEFAULT && widthHint + widthAdjustment < minWidth) {
            if (heightHint == SWT.DEFAULT) {
                return new Point(minWidth, computeHeightAtMinimumWidth());   
            }
            
            widthHint = minWidth - widthAdjustment;
        }
        
        // If we're asking for a result smaller than the minimum height
        int minHeight = computeMinimumHeight();
        
        if (heightHint != SWT.DEFAULT && heightHint + heightAdjustment < minHeight) {
            if (widthHint == SWT.DEFAULT) {
                return new Point(computeWidthAtMinimumHeight(), minHeight);
            }
            
            heightHint = minHeight - heightAdjustment;
        }
        
        // If both dimensions were supplied in the input, compute the trivial result
        if (widthHint != SWT.DEFAULT && heightHint != SWT.DEFAULT) {                        
            return new Point(widthHint + widthAdjustment, heightHint + heightAdjustment);
        }

        // 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 + widthAdjustment;
            }

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

            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 + widthAdjustment == preferredSize.x) {
                    return Geometry.copy(preferredSize);
                }
            }

            // If we have a cached height measurement
            if (cachedHeightQuery != -1) {
                // If this was measured with the same width hint
                if (cachedHeightQuery == widthHint) {
                    return new Point(widthHint + widthAdjustment, cachedHeightResult);
                }
            }

            // 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 + widthAdjustment >= preferredSize.x) {
                    return new Point(widthHint + widthAdjustment, preferredSize.y);
                }
            }

            // Else we can't find an existing size in the cache, so recompute
            // it from scratch.
            Point newHeight = controlComputeSize(widthHint - widthAdjustment, SWT.DEFAULT);

            cachedHeightQuery = heightHint;
            cachedHeightResult = newHeight.y;
            
            return newHeight;
        }

        // 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 + heightAdjustment == preferredSize.y) {
                    return Geometry.copy(preferredSize);
                }
            }

            // If we have a cached width measurement with the same height hint
            if (cachedWidthQuery == heightHint) {
                return new Point(cachedWidthResult, heightHint + heightAdjustment);
            }

            Point widthResult = controlComputeSize(SWT.DEFAULT, heightHint - heightAdjustment);

            cachedWidthQuery = heightHint;
            cachedWidthResult = widthResult.x;
            
            return widthResult;
        }

        return controlComputeSize(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 widthHint the horizontal hint
     * @param heightHint the vertical hint
     * @return the control's size
     */
    public Point computeAdjustedSize(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 = computeSize(adjustedWidthHint, adjustedHeightHint);

        // 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.
        
        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 || control.isDisposed()) {
            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 Scrollable) {
            // For scrollables, subtract off the trim size
            Scrollable scrollable = (Scrollable) control;
            Rectangle trim = scrollable.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;
        }
    }

    private Point controlComputeSize(int widthHint, int heightHint) {
        Point result = control.computeSize(widthHint, heightHint, flushChildren);
        flushChildren = false;
        
        return result;
    }

    /**
     * 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);
    }
    
    public int computeMinimumWidth() {
        if (minimumWidth == -1) {
    		if (control instanceof Composite) {
    			Layout layout = ((Composite)control).getLayout();
    			if (layout instanceof ILayoutExtension) {
    				minimumWidth = ((ILayoutExtension)layout).computeMinimumWidth((Composite)control, flushChildren);
    				flushChildren = false;
    			}
    		}
        }

        if (minimumWidth == -1) {
            Point minWidth = controlComputeSize(FormUtil.getWidthHint(5, control), SWT.DEFAULT); 
            minimumWidth = minWidth.x;
            heightAtMinimumWidth = minWidth.y;
        }
		
		return minimumWidth;
    }
    
    public int computeMaximumWidth() {
        if (maximumWidth == -1) {
    		if (control instanceof Composite) {
    			Layout layout = ((Composite)control).getLayout();
    			if (layout instanceof ILayoutExtension) {
    				maximumWidth = ((ILayoutExtension)layout).computeMaximumWidth((Composite)control, flushChildren);
    				flushChildren = false;
    			}
    		}
        }

        if (maximumWidth == -1) {
            maximumWidth = getPreferredSize().x;
        }
		
        return maximumWidth;
	}
    
    private int computeHeightAtMinimumWidth() {
        int minimumWidth = computeMinimumWidth();
        
        if (heightAtMinimumWidth == -1) {
            heightAtMinimumWidth = controlComputeSize(minimumWidth - widthAdjustment, SWT.DEFAULT).y;
        }
        
        return heightAtMinimumWidth;
    }
    
    private int computeWidthAtMinimumHeight() {
        int minimumHeight = computeMinimumHeight();
        
        if (widthAtMinimumHeight == -1) {
            widthAtMinimumHeight = controlComputeSize(SWT.DEFAULT, minimumHeight - heightAdjustment).x;
        }
        
        return widthAtMinimumHeight;
    }

    private int computeMinimumHeight() {
        if (minimumHeight == -1) {
            Point sizeAtMinHeight = controlComputeSize(SWT.DEFAULT, 0);
            
            minimumHeight = sizeAtMinHeight.y;
            widthAtMinimumHeight = sizeAtMinHeight.x;
        }
        
        return minimumHeight;
    }
    
    public Point computeMinimumSize() {
        return new Point(computeMinimumWidth(), computeMinimumHeight());
    }

    public void setSize(Point newSize) {
        if (control != null) {
            control.setSize(newSize);
        }
        
        layoutIfNecessary();
    }
    
    public void setSize(int width, int height) {
        if (control != null) {
            control.setSize(width, height);
        }
        
        layoutIfNecessary();        
    }
    
    public void setBounds(int x, int y, int width, int height) {
        if (control != null) {
            control.setBounds(x, y, width, height);
        }
        
        layoutIfNecessary();        
    }
    
    public void setBounds(Rectangle bounds) {
        if (control != null) {
            control.setBounds(bounds);
        }
        
        layoutIfNecessary();
    }

    public void layoutIfNecessary() {
        if (dirtySize != null && control != null && control instanceof Composite) {
            if (control.getSize().equals(dirtySize)) {
	            ((Composite)control).layout(flushChildren);
	            flushChildren = false;
            }
        }
        dirtySize = null;
    }
}
