/*******************************************************************************
 * Copyright (c) 2004 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.internal.presentations.util;

import org.eclipse.jface.util.Geometry;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.ControlListener;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Layout;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.ui.internal.dnd.DragUtil;
import org.eclipse.ui.internal.dnd.SwtUtil;
import org.eclipse.ui.internal.layout.SizeCache;

/**
 * A ProxyControl is an invisible control whose size and position are linked
 * with some target control. That is, when the dummy control is asked for its
 * preferred size it returns the preferred size of the target. Changing the
 * bounds of the dummy control also changes the bounds of the target. This allows 
 * any Composite to lay out a control that isn't one of its children.
 * 
 * <p>
 * For example, imagine you have a ViewForm and a ToolBar that share the same parent
 * and you want the ToolBar to be located in the upper-right corner of the ViewForm.
 * If the ToolBar were a child of the ViewForm, this could be done easily by calling
 * viewForm.setTopRight(toolBar). However, this is impossible since ViewForm.setTopRight
 * will only accept a child control. Instead, we create a ProxyControl as a child
 * of the viewForm, and set the toolbar as its target. The ViewForm will treat
 * the ProxyControl just like any other child, but it will actually be arranging the
 * ToolBar. 
 * </p>
 * <p>For example:
 * </p>
 * <code>
 *      // Create a ViewForm and a ToolBar that are siblings
 * 		ViewForm viewForm = new ViewForm(parent, SWT.NONE);
 * 		ToolBar toolBar = new ToolBar(parent, SWT.NONE);
 * 
 *      // Allow the ViewForm to control the position of the ToolBar by creating
 *      // a ProxyControl in the ViewForm that targets the ToolBar.
 * 		ProxyControl toolBarProxy = new ProxyControl(viewForm);
 * 		toolBarProxy.setTarget(toolBar);
 * 		viewForm.setTopRight(toolBarProxy.getControl());
 * </code>
 * 
 * <p>
 * This is intended to simplify management of view toolbars in the presentation API.
 * Presentation objects have no control over where the view toolbars are created in
 * the widget hierarchy, but they may wish to control the position of the view toolbars
 * using traditional SWT layouts and composites. 
 * </p>
 */
public class ProxyControl {
    
    /**
     * Invisible dummy control 
     */
	private Composite control;
	
	/**
	 * Target control (possibly null)
	 */
    private Control target = null;
    
    /**
     * Target cache (possibly null)
     */
    private SizeCache targetCache = null;
    
	/**
	 * Most specific common ancestor between the target and the proxy controls
	 */
	private Control commonAncestor;
	
	/**
	 * Visibility state of the proxy control the last time it had a non-null target.
	 * Note: when the target is set to null, we force the proxy to become invisible
	 * and use this variable to remember the initial state when we get a new non-null
	 * target.
	 */
	private boolean visible = true;
    
	/**
	 * Dispose listener. Breaks the link between the target and the proxy if either
	 * control is disposed.
	 */
	private DisposeListener disposeListener = new DisposeListener() {
		public void widgetDisposed(DisposeEvent e) {
			if (e.widget == target || e.widget == control) {
				setTargetControl(null);
			}
		}
	};
	
	private Listener visibilityListener = new Listener() {

        public void handleEvent(Event event) {
            if (target != null) {
                visible = control.getVisible();
                target.setVisible(visible);
            }
        }
	    
	};
    
	/**
	 * Movement listener. Updates the bounds of the target to match the 
	 * bounds of the dummy control.
	 */
	private ControlListener controlListener = new ControlListener() {

		public void controlMoved(ControlEvent e) {
			ProxyControl.this.layout();
		}

		public void controlResized(ControlEvent e) {
		    //if (e.widget == control) {
		     //   ProxyControl.this.layout();
		    //}
		}
		
	};
	
	/**
	 * Creates a new ProxyControl as a child of the given parent. This is an invisible dummy
	 * control. If given a target, the ProxyControl will update the bounds of the target to
	 * match the bounds of the dummy control.
	 * 
	 * @param parent parent composite
	 */
	public ProxyControl(Composite parent) {
	    // Create the invisible dummy composite
		control = new Composite(parent, SWT.NO_BACKGROUND);
		control.setVisible(false);
		
		// Attach a layout to the dummy composite. This is used to make the preferred
		// size of the dummy match the preferred size of the target.
		control.setLayout(new Layout() {
			protected void layout (Composite composite, boolean flushCache) {
			    ProxyControl.this.layout();
			    // does nothing. The bounds of the target are updated by the controlListener
			}
			
			protected Point computeSize (Composite composite, int wHint, int hHint, boolean flushCache) {
				if (targetCache == null) {
				    // Note: If we returned (0,0), SWT would ignore the result and use a default value.
					return new Point(1,1);
				}
                
				return targetCache.computeSize(wHint, hHint);
			}
		});
		
		// Attach listeners to the dummy
		control.addDisposeListener(disposeListener);
		control.addListener(SWT.Show, visibilityListener);
		control.addListener(SWT.Hide, visibilityListener);
	}
	
	/**
	 * Sets the control whose position will be managed by this proxy
	 * 
	 * @param target the control, or null if none
	 */
	public void setTargetControl(Control target) {
		if (this.target != target) {

		    if (this.target != null) {
		        for (Control next = control; next != commonAncestor && next != null; next = next.getParent()) {
		            next.removeControlListener(controlListener);
		        }
		        commonAncestor = null;
		        
			    // If we already had a target, detach the dispose listener 
			    // (prevents memory leaks due to listeners)
				if (!this.target.isDisposed()) {
					this.target.removeDisposeListener(disposeListener);
				}				
		    }
			
			if (this.target == null && target != null) {
			    // If we had previously forced the dummy control invisible, restore its visibility
			    control.setVisible(visible);
			}
			
			this.target = target;
			
			if (target != null) {
			    commonAncestor = SwtUtil.findCommonAncestor(this.target, control);
		        for (Control next = control; next != null && next != commonAncestor; next = next.getParent()) {
		            next.addControlListener(controlListener);
		        }
			    
			    // Make the new target's visiblity match the visibility of the dummy control
			    target.setVisible(control.getVisible());
				// Add a dispose listener. Ensures that the target is cleared
				// if it is ever disposed.
				target.addDisposeListener(disposeListener);
			} else {
			    control.setVisible(false);
			}
		}
	}
	
    public void setTarget(SizeCache cache) {
        targetCache = cache;
        
        if (targetCache != null) {
            setTargetControl(cache.getControl());
        } else {
            setTargetControl(null);
        }
    }
    
	/**
	 * Returns the proxy control
	 * 
	 * @return the proxy control (not null)
	 */
	public Control getControl() {
		return control;
	}
	
	public Control getTarget() {
        return target;
	}
	
	/**
	 * Moves the target control on top of the dummy control.
	 */
	public void layout() {
		if (getTarget() == null) {
			return;
		}
		
		// Compute the unclipped bounds of the target in display coordinates
		Rectangle displayBounds = Geometry.toDisplay(control.getParent(), control.getBounds());
		
		// Clip the bounds of the target so that it doesn't go outside the dummy control's parent
		Rectangle clippingRegion = DragUtil.getDisplayBounds(control.getParent());
		displayBounds = displayBounds.intersection(clippingRegion);
		
		// Compute the bounds of the target, in the local coordinate system of its parent
		Rectangle targetBounds = Geometry.toControl(getTarget().getParent(), displayBounds);
		
		// Move the target
		getTarget().setBounds(targetBounds);
	}
}
