/*
 * Copyright (c) 2005-2009 Laf-Widget Kirill Grouchnikov. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 * 
 *  o Redistributions of source code must retain the above copyright notice, 
 *    this list of conditions and the following disclaimer. 
 *     
 *  o Redistributions in binary form must reproduce the above copyright notice, 
 *    this list of conditions and the following disclaimer in the documentation 
 *    and/or other materials provided with the distribution. 
 *     
 *  o Neither the name of Laf-Widget Kirill Grouchnikov nor the names of 
 *    its contributors may be used to endorse or promote products derived 
 *    from this software without specific prior written permission. 
 *     
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 */
package org.jvnet.lafwidget.layout;

import java.awt.*;
import java.util.*;

import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.plaf.UIResource;
import javax.swing.tree.TreePath;

import org.jvnet.lafwidget.animation.*;

/**
 * Transition layout. The public methods in this class that are not implementing
 * the {@link LayoutManager} are for look-and-feel usage. Application code
 * should use the {@link #getAlphaComposite(Component)} and
 * {@link #getAlphaComposite(Component, float)} methods only in custom painting
 * code (overriding the {@link JComponent#paintComponent(Graphics)} method.
 * 
 * @author Kirill Grouchnikov.
 */
public class TransitionLayout implements LayoutManager {
	/**
	 * The original layout manager. Handles the layout-related tasks.
	 */
	protected LayoutManager delegate;

	protected java.util.List eventListeners;

	/**
	 * Client property that specifies the current transition state of a
	 * component. The value is a correct multiplication of all transitions that
	 * a component participates in.
	 * 
	 * @see #OWN_ALPHA
	 * @see #getCompositeAlpha(Component)
	 * @see #getAlphaComposite(Component)
	 * @see #getAlphaComposite(Component, float)
	 */
	public static final String ALPHA = "lafwidgets.layout.alpha";

	/**
	 * Client property that specifies the transition state of a component in a
	 * transition that happens directly on that component.
	 * 
	 * @see #ALPHA
	 * @see #getCompositeAlpha(Component)
	 * @see #getAlphaComposite(Component)
	 * @see #getAlphaComposite(Component, float)
	 */
	public static final String OWN_ALPHA = "lafwidgets.layout.ownAlpha";

	/**
	 * Client property to store the original opacity of the component while it
	 * is in a transition. The {@link #isOpaque(Component)} uses this property
	 * to correctly report the "real" component opacity to the painting code.
	 */
	public static final String ORIGINAL_OPACITY = "lafwidgets.layout.originalOpacity";

	// public static final String IGNORE = "lafwidgets.layout.ignore";

	/**
	 * Client property to store the current visibility of components. Since we
	 * are playing with calls to {@link Component#setVisible(boolean)}, this
	 * property tracks the "real" visibility.
	 */
	public static final String SHOWING = "lafwidgets.layout.showing";

	/**
	 * Client property that marks components in fade-out state. Such components
	 * are hidden before layout out the container and reshown afterwards.
	 */
	public static final String LIMBO = "lafwidgets.layout.limbo";

	/**
	 * Client property for storing the current bounds of a component. This is
	 * used to perform animations on components that stay visible but change
	 * location.
	 */
	public static final String BOUNDS = "lafwidgets.layout.bounds";

	/**
	 * The associated container.
	 */
	protected Container container;

	protected boolean doImmediateRepaint;

	/**
	 * Fade kind for animating the change in component bounds.
	 */
	public static final FadeKind COMPONENT_BOUNDS = new FadeKind(
			"lafwidgets.layout.componentBounds");

	/**
	 * Fade kind for animating the change in component visibility.
	 */
	public static final FadeKind COMPONENT_FADE = new FadeKind(
			"lafwidgets.layout.componentFade");

	// protected Set added;
	//
	// protected Set removed;
	//	
	protected boolean hasFades;

	protected boolean hasPendingLayoutRequests;

	protected int pendingAnimationCount;

	public TransitionLayout(Container container, LayoutManager delegate,
			boolean hasFades) {
		super();
		this.container = container;
		this.delegate = delegate;
		this.hasFades = hasFades;
		this.doImmediateRepaint = false;

		this.hasPendingLayoutRequests = false;
		this.pendingAnimationCount = 0;

		this.eventListeners = new ArrayList();

		// added = new HashSet();
		// removed = new HashSet();
		// this.container.addContainerListener(new ContainerAdapter() {
		// public void componentRemoved(ContainerEvent e) {
		// removed.add(e.getChild());
		// }
		//
		// public void componentAdded(ContainerEvent e) {
		// final Component c = e.getChild();
		// added.add(c);
		// c.addHierarchyListener(new HierarchyListener() {
		// public void hierarchyChanged(HierarchyEvent e) {
		// if ((e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0) {
		// Component c = e.getComponent();
		// if (e.getChanged().isVisible()) {
		// added.add(c);
		// removed.remove(c);
		// // System.out.println(c + " added");
		// } else {
		// removed.add(c);
		// added.remove(c);
		// // System.out.println(c + " removed");
		// }
		// }
		// }
		// });
		// }
		// });
	}

	public void addLayoutComponent(String name, Component comp) {
		delegate.addLayoutComponent(name, comp);
	}

	public void layoutContainer(Container parent) {
		// SimpleDateFormat sdf = new SimpleDateFormat("hh:mm:ss.SSS");
		// System.out.println("Layout at " + sdf.format(new Date()));
		// System.out.println("\tbefore");

		if (this.getPendingAnimationCount() > 0) {
			this.requestLayout();
			return;
		}

		fireEvent(null, TransitionLayoutEvent.TRANSITION_STARTED);
		this.installBorders(container);

		final Map oldLocations = new HashMap();
		for (int i = 0; i < parent.getComponentCount(); i++) {
			Component c = parent.getComponent(i);
			JComponent jc = (JComponent) c;
			if (!jc.isVisible()
					|| (hasFades && !Boolean.TRUE.equals(jc
							.getClientProperty(SHOWING))))
				setAlpha(jc, new Float(0.0), new Float(0.0), true);
			if (Boolean.TRUE.equals(jc.getClientProperty(LIMBO)))
				jc.setVisible(false);
			// System.out.println("before : " + c.getClass().getName() + " ["
			// + c.isVisible() + "] " + c.getBounds());
			oldLocations.put(jc, new Rectangle(jc.getBounds()));
			// jc.putClientProperty(ALPHA, new Float(0.0));
		}
		final Map<Component, Boolean> parentOpacityMap = new HashMap<Component, Boolean>();
		if (hasFades)
			makeNonOpaque(parent, parentOpacityMap);
		delegate.layoutContainer(parent);
		if (hasFades) {
			restoreOpaque(parent, parentOpacityMap, true);
			if (parentOpacityMap.size() > 0) {
				throw new IllegalStateException();
			}
		}
		parent.repaint();
		// System.out.println("Has " + parent.getComponentCount() + " comps");
		for (int i = 0; i < parent.getComponentCount(); i++) {
			final Component comp = parent.getComponent(i);
			final JComponent jc = (JComponent) comp;
			// System.out.println("after : " + jc.getClass().getName() + " ["
			// + jc.isVisible() + "] " + jc.getBounds());
			final Rectangle newBounds = new Rectangle(jc.getBounds());
			final Rectangle oldBounds = (jc.getClientProperty(BOUNDS) instanceof Rectangle) ? (Rectangle) jc
					.getClientProperty(BOUNDS)
					: (Rectangle) oldLocations.get(jc);
			// boolean wasShowing = ((oldBounds.width > 0) && (oldBounds.height
			// > 0))
			// || removed.contains(c);
			boolean wasShowing = Boolean.TRUE.equals(jc
					.getClientProperty(SHOWING))
					&& !FadeTracker.getInstance().isTracked(jc, COMPONENT_FADE);
			boolean isShowing = /*
								 * ((newBounds.width > 0) && (newBounds.height >
								 * 0) &&
								 */jc.isVisible()
					&& (!Boolean.TRUE.equals(jc.getClientProperty(LIMBO)));
			// || added.contains(c);

			// if (jc instanceof JButton) {
			// System.out.println(((JButton) jc).getText() + " " + wasShowing
			// + "->" + isShowing);
			// }
			//
			if (jc.isVisible())
				jc.putClientProperty(SHOWING, Boolean.TRUE);
			else
				jc.putClientProperty(SHOWING, null);

			jc.putClientProperty(BOUNDS, new Rectangle(newBounds));
			if (Boolean.TRUE.equals(jc.getClientProperty(LIMBO)))
				jc.setVisible(true);

			if (!isShowing && !wasShowing) {
				clearAlpha(jc, true);
			}

			// removed.remove(c);
			// added.remove(c);
			if (isShowing && wasShowing) {
				// if (oldBounds.equals(newBounds)
				// && !FadeTracker.getInstance().isTracked(jc,
				// COMPONENT_FADE))
				// setAlpha(jc, null);
				// jc.putClientProperty(ALPHA, null);
				if (!oldBounds.equals(newBounds)) {
					jc.setBounds(oldBounds);
					final Map<Component, Boolean> opacity = new HashMap<Component, Boolean>();
					if (hasFades)
						makeNonOpaque(jc, opacity);
					// if (jc instanceof JButton) {
					// System.out.println(((JButton) jc).getText() + " "
					// + oldBounds + "->" + newBounds);
					// }
					this.animationStarted();
					FadeTracker.getInstance().trackFadeIn(COMPONENT_BOUNDS, jc,
							false, new UIThreadFadeTrackerAdapter() {
								@Override
								public void fadeEnded(FadeKind fadeKind) {
									if (hasFades) {
										restoreOpaque(jc, opacity, true);
										if (opacity.size() > 0) {
											throw new IllegalStateException();
										}
									}
									float parentAlpha = getCompositeAlpha(jc
											.getParent());
									if (parentAlpha < 1.0f)
										setAlpha(jc, new Float(parentAlpha),
												new Float(1.0f), true);
									else
										clearAlpha(jc, true);
									animationEnded();
								}

								@Override
								public void fadePerformed(FadeKind fadeKind,
										final float fade) {
									// if (fade == 1.0)
									Rectangle currBounds = new Rectangle(
											(int) (oldBounds.x + fade
													* (newBounds.x - oldBounds.x)),
											(int) (oldBounds.y + fade
													* (newBounds.y - oldBounds.y)),
											(int) (oldBounds.width + fade
													* (newBounds.width - oldBounds.width)),
											(int) (oldBounds.height + fade
													* (newBounds.height - oldBounds.height)));
									// System.out.println(fade + ":"
									// + currBounds + ":" + oldBounds
									// + ":" + newBounds);
									jc.setBounds(currBounds);
									fireEvent(jc,
											TransitionLayoutEvent.CHILD_MOVING);
									jc.doLayout();

									if (hasFades) {
										// &&
										// (jc.getClientProperty(ALPHA)
										// ==
										// null)) {
										double coef = 1 + 2.0 * Math
												.abs(fade - 0.5);
										double alpha = 0.5 + (0.25 * coef * coef) / 2.0;
										setAlpha(jc, new Float(
												getCompositeAlpha(jc
														.getParent())
														* alpha), new Float(
												alpha), true);
										// jc.putClientProperty(ALPHA,
										// new
										// Float(
										// alpha));
									}
									repaint(jc);
									// jc.repaint();
								}
							});
				}
			}
			if (!wasShowing && isShowing && hasFades) {
				setAlpha(jc, new Float(0.0), new Float(0.0), true);
				// jc.putClientProperty(ALPHA, new Float(0.0f));
				jc.setBounds(newBounds);
				final Map<Component, Boolean> opacity = new HashMap<Component, Boolean>();
				makeNonOpaque(jc, opacity);
				jc.setVisible(true);
				this.animationStarted();
				FadeTracker.getInstance().trackFadeIn(COMPONENT_FADE, jc,
						false, new UIThreadFadeTrackerAdapter() {
							@Override
							public void fadeEnded(FadeKind fadeKind) {
								float parentAlpha = getCompositeAlpha(jc
										.getParent());
								if (parentAlpha < 1.0f)
									setAlpha(jc, new Float(parentAlpha),
											new Float(1.0f), true);
								else
									clearAlpha(jc, true);
								restoreOpaque(jc, opacity, true);
								if (opacity.size() > 0) {
									throw new IllegalStateException();
								}
								animationEnded();
							}

							@Override
							public void fadePerformed(FadeKind fadeKind,
									final float fade) {
								setAlpha(jc, new Float(getCompositeAlpha(jc
										.getParent())
										* fade), new Float(fade), true);
								// jc.putClientProperty(ALPHA, new
								// Float(
								// fade));
								// if (jc instanceof JDesktopPane)
								// System.out.println(jc.getClass().getName()
								// + " --> "
								// + jc.getClientProperty(ALPHA));
								fireEvent(jc,
										TransitionLayoutEvent.CHILD_FADING_IN);
								repaint(jc);
								// jc.repaint();
							}
						});
			}
			if (!isShowing && wasShowing && hasFades) {
				float parentAlpha = getCompositeAlpha(jc.getParent());
				if (parentAlpha < 1.0f)
					setAlpha(jc, new Float(parentAlpha), new Float(1.0f), true);
				else
					setAlpha(jc, new Float(1.0f), new Float(1.0f), true);
				// jc.putClientProperty(ALPHA, new Float(1.0f));
				final Map<Component, Boolean> opacity = new HashMap<Component, Boolean>();
				makeNonOpaque(jc, opacity);
				jc.setBounds(oldBounds);
				jc.putClientProperty(LIMBO, Boolean.TRUE);
				jc.setVisible(true);
				this.animationStarted();
				FadeTracker.getInstance().trackFadeOut(COMPONENT_FADE, jc,
						false, new UIThreadFadeTrackerAdapter() {
							@Override
							public void fadeEnded(FadeKind fadeKind) {
								restoreOpaque(jc, opacity, true);
								if (opacity.size() > 0) {
									throw new IllegalStateException();
								}
								jc.setBounds(newBounds);
								jc.setVisible(false);
								jc.putClientProperty(SHOWING, null);
								jc.putClientProperty(LIMBO, null);
								clearAlpha(jc, true);
								// jc.putClientProperty(IGNORE,
								// Boolean.TRUE);
								animationEnded();
							}

							@Override
							public void fadePerformed(FadeKind fadeKind,
									final float fade) {
								setAlpha(jc, new Float(getCompositeAlpha(jc
										.getParent())
										* fade), new Float(fade), true);
								// jc.putClientProperty(ALPHA, new
								// Float(
								// fade));
								fireEvent(jc,
										TransitionLayoutEvent.CHILD_FADING_OUT);
								repaint(jc);
								// jc.repaint();
							}
						});
			}
		}
		// added.clear();
		// removed.clear();

		// if (isShowing && !wasShowing) {
		// if (c instanceof JComponent) {
		// ((JComponent) c).putClientProperty(ALPHA, new Double(0.0f));
		// }
		// }
		if (this.getPendingAnimationCount() == 0)
			this.fireEvent(null, TransitionLayoutEvent.TRANSITION_ENDED);
	}

	public Dimension minimumLayoutSize(Container parent) {
		return delegate.minimumLayoutSize(parent);
	}

	public Dimension preferredLayoutSize(Container parent) {
		return delegate.preferredLayoutSize(parent);
	}

	public void removeLayoutComponent(Component comp) {
		delegate.removeLayoutComponent(comp);
	}

	/**
	 * Makes the specified component and all its descendants non-opaque.
	 * 
	 * @param comp
	 *            Component.
	 * @param opacitySnapshot
	 *            The "snapshot" map that will contain the original opacity
	 *            status of the specified component and all its descendants.
	 */
	public static void makeNonOpaque(Component comp,
			Map<Component, Boolean> opacitySnapshot) {
		if (comp instanceof JComponent) {
			JComponent jcomp = (JComponent) comp;
			opacitySnapshot.put(comp, jcomp.isOpaque());
			jcomp.putClientProperty(ORIGINAL_OPACITY, Boolean.valueOf(jcomp
					.isOpaque()));
			// System.out.println(jcomp.getClass().getName() + " : "
			// + jcomp.isOpaque() + " --> false");
			jcomp.setOpaque(false);
		}

		if (comp instanceof Container) {
			Container cont = (Container) comp;
			for (int i = 0; i < cont.getComponentCount(); i++)
				makeNonOpaque(cont.getComponent(i), opacitySnapshot);
		}
	}

	/**
	 * Restores the opacity of the specified component and all its descendants.
	 * 
	 * @param comp
	 *            Component.
	 * @param opacitySnapshot
	 *            The "snapshot" map that contains the original opacity status
	 *            of the specified component and all its descendants.
	 */
	public static void restoreOpaque(Component comp,
			Map<Component, Boolean> opacitySnapshot, boolean toCleanup) {
		if (comp instanceof JComponent) {
			JComponent jcomp = (JComponent) comp;
			// snapshot may not contain opacity for table header of a table when
			// it's used inside tree cell renderer (wrapper in a scroll pane).
			if (opacitySnapshot.containsKey(jcomp)) {
				jcomp.setOpaque(opacitySnapshot.get(jcomp));
				opacitySnapshot.remove(jcomp);
			} else
				jcomp.setOpaque(true);
			jcomp.putClientProperty(ORIGINAL_OPACITY, null);
			// System.out.println(jcomp.getClass().getName() + " --> "
			// + jcomp.isOpaque());
		}
		if (comp instanceof Container) {
			Container cont = (Container) comp;
			for (int i = 0; i < cont.getComponentCount(); i++)
				restoreOpaque(cont.getComponent(i), opacitySnapshot, false);
		}

		if (toCleanup) {
			for (Map.Entry<Component, Boolean> entry : opacitySnapshot
					.entrySet()) {
				if (entry.getKey() instanceof JComponent) {
					JComponent jcomp = (JComponent) entry.getKey();
					jcomp.setOpaque(entry.getValue());
					jcomp.putClientProperty(ORIGINAL_OPACITY, null);
				}
			}
			opacitySnapshot.clear();
		}
	}

	public static void setAlpha(Component comp, Float alpha, Float ownAlpha,
			boolean main) {
		// if (main) {
		// System.out.println(comp.getClass().getName() + "[@"
		// + comp.hashCode() + "] " + " --> " + alpha);
		// }
		if (comp instanceof JComponent) {
			JComponent jcomp = (JComponent) comp;
			// else
			// {
			// System.out.println("Setting " + comp.getClass().getName()
			// + " to " + alpha);
			// }
			// if (jcomp instanceof JDesktopPane)
			// System.out.println(jcomp.getClass().getName() + " --> "
			// + jcomp.getClientProperty(ALPHA));

			if (main) {
				jcomp.putClientProperty(OWN_ALPHA, ownAlpha);
			} else {
				// Here we are on a child component of fading parent. If the
				// child component itself is in fading animation, that animation
				// will correctly set the alpha based on the parent chain
				// translucency.
				// Because of this we simply return, not going to the children
				// of the current component (which will be / already were
				// traversed in the fade loop of the current component.
				if (FadeTracker.getInstance().isTracked(jcomp, COMPONENT_FADE)) {
					return;
				}
			}
			// if (jcomp instanceof JButton) {
			// JButton jb = (JButton) jcomp;
			// if (jb.getText().equals("2"))
			// System.out.println("Setting '"
			// + ((JButton) jcomp).getText() + "' to " + alpha);
			// }
			jcomp.putClientProperty(ALPHA, alpha);
		}

		if (comp instanceof JList) {
			JList list = (JList) comp;
			for (int i = 0; i < list.getModel().getSize(); i++) {
				Component rendComp = list.getCellRenderer()
						.getListCellRendererComponent(list,
								list.getModel().getElementAt(i), i,
								list.isSelectedIndex(i), false);
				setAlpha(rendComp, alpha, null, false);
			}
		}

		if (comp instanceof JTree) {
			JTree tree = (JTree) comp;
			for (int row = 0; row < tree.getRowCount(); row++) {
				TreePath path = tree.getPathForRow(row);
				boolean isLeaf = tree.getModel().isLeaf(
						path.getLastPathComponent());

				Component rendComp = tree.getCellRenderer()
						.getTreeCellRendererComponent(tree,
								path.getLastPathComponent(),
								tree.isRowSelected(row), tree.isExpanded(row),
								isLeaf, row, false);

				setAlpha(rendComp, alpha, null, false);
			}
		}

		if (comp instanceof JTable) {
			JTable table = (JTable) comp;
			for (int i = 0; i < table.getRowCount(); i++) {
				for (int j = 0; j < table.getColumnCount(); j++) {
					Component rendComp = table.getCellRenderer(i, j)
							.getTableCellRendererComponent(table,
									table.getValueAt(i, j),
									table.isCellSelected(i, j), false, i, j);
					setAlpha(rendComp, alpha, null, false);
				}
			}
		}

		if (comp instanceof Container) {
			Container cont = (Container) comp;
			for (int i = 0; i < cont.getComponentCount(); i++)
				setAlpha(cont.getComponent(i), alpha, null, false);
		}
	}

	protected static void clearAlpha(Component comp, boolean main) {
		// if (main) {
		// System.out.println(comp.getClass().getName() + "[@"
		// + comp.hashCode() + "] " + " --> " + alpha);
		// }
		if (comp instanceof JComponent) {
			JComponent jcomp = (JComponent) comp;
			jcomp.putClientProperty(OWN_ALPHA, null);
			if (!main) {
				// Here we are on a child component of a parent that finished
				// its fading. If the child component itself is in fading
				// animation, we do not reset its alpha.
				// Because of this we simply return, not going to the children
				// of the current component (which will be / already were
				// traversed in the fade loop of the current component.
				if (FadeTracker.getInstance().isTracked(jcomp, COMPONENT_FADE)) {
					return;
				}
			}
			jcomp.putClientProperty(ALPHA, null);
		}

		if (comp instanceof JList) {
			JList list = (JList) comp;
			for (int i = 0; i < list.getModel().getSize(); i++) {
				Component rendComp = list.getCellRenderer()
						.getListCellRendererComponent(list,
								list.getModel().getElementAt(i), i,
								list.isSelectedIndex(i), false);
				clearAlpha(rendComp, false);
			}
		}

		if (comp instanceof JTree) {
			JTree tree = (JTree) comp;
			for (int row = 0; row < tree.getRowCount(); row++) {
				TreePath path = tree.getPathForRow(row);
				boolean isLeaf = tree.getModel().isLeaf(
						path.getLastPathComponent());

				Component rendComp = tree.getCellRenderer()
						.getTreeCellRendererComponent(tree,
								path.getLastPathComponent(),
								tree.isRowSelected(row), tree.isExpanded(row),
								isLeaf, row, false);

				clearAlpha(rendComp, false);
			}
		}

		if (comp instanceof JTable) {
			JTable table = (JTable) comp;
			for (int i = 0; i < table.getRowCount(); i++) {
				for (int j = 0; j < table.getColumnCount(); j++) {
					Component rendComp = table.getCellRenderer(i, j)
							.getTableCellRendererComponent(table,
									table.getValueAt(i, j),
									table.isCellSelected(i, j), false, i, j);
					clearAlpha(rendComp, false);
				}
			}
		}

		if (comp instanceof Container) {
			Container cont = (Container) comp;
			for (int i = 0; i < cont.getComponentCount(); i++)
				clearAlpha(cont.getComponent(i), false);
		}
	}

	private synchronized void requestLayout() {
		this.hasPendingLayoutRequests = true;
	}

	private synchronized void layoutFinished() {
		this.hasPendingLayoutRequests = false;
		if (this.getPendingAnimationCount() == 0) {
			fireEvent(null, TransitionLayoutEvent.TRANSITION_ENDED);
		}
	}

	private synchronized boolean hasPendingLayoutRequests() {
		return this.hasPendingLayoutRequests;
	}

	private synchronized int getPendingAnimationCount() {
		return this.pendingAnimationCount;
	}

	private synchronized void animationStarted() {
		this.pendingAnimationCount++;
	}

	private synchronized void animationEnded() {
		this.pendingAnimationCount--;
		if (this.pendingAnimationCount == 0) {
			if (this.hasPendingLayoutRequests()) {
				SwingUtilities.invokeLater(new Runnable() {
					public void run() {
						layoutContainer(container);
						layoutFinished();
						container.repaint();
						// container.repaint();
					}
				});
			} else {
				fireEvent(null, TransitionLayoutEvent.TRANSITION_ENDED);
			}
		}
	}

	public LayoutManager getDelegate() {
		return delegate;
	}

	void installBorders(Component comp) {
		if (comp instanceof JComponent) {
			JComponent jcomp = ((JComponent) comp);
			Border border = jcomp.getBorder();
			if (border != null) {
				if (!(border instanceof TransitionBorder)) {
					// System.out.println("Setting TB on "
					// + jcomp.getClass().getSimpleName() + "["
					// + jcomp.hashCode() + "] with "
					// + border.getClass().getName());
					if (border instanceof UIResource)
						jcomp.setBorder(new TransitionBorder.BorderUIResource(
								border));
					else
						jcomp.setBorder(new TransitionBorder(border));
				}
			}
		}
		if (comp instanceof Container) {
			Container cont = (Container) comp;
			for (int i = 0; i < cont.getComponentCount(); i++) {
				installBorders(cont.getComponent(i));
			}
		}
	}

	void uninstallBorders(Component comp) {
		if (comp instanceof JComponent) {
			JComponent jcomp = ((JComponent) comp);
			Border border = jcomp.getBorder();
			if (border instanceof TransitionBorder) {
				jcomp.setBorder(((TransitionBorder) border).getDelegate());
				// System.out.println("Restored on "
				// + jcomp.getClass().getSimpleName() + "["
				// + jcomp.hashCode() + "] "
				// + jcomp.getBorder().getClass().getName());
			}
		}
		if (comp instanceof Container) {
			Container cont = (Container) comp;
			for (int i = 0; i < cont.getComponentCount(); i++) {
				uninstallBorders(cont.getComponent(i));
			}
		}
	}

	/**
	 * Returns the composite to use for painting the specified component. The
	 * result should be set on the {@link Graphics2D} before any custom
	 * rendering is done. This method can be used by application painting code
	 * and by look-and-feel delegates.
	 * 
	 * @param c
	 *            Component.
	 * @param translucency
	 *            The translucency of the original painting (when the component
	 *            is not under any transition fade effect).
	 * @param g
	 *            The original graphics context.
	 * @return The composite to use for painting the specified component.
	 */
	public static Composite getAlphaComposite(Component c, float translucency,
			Graphics g) {
		float xFactor = 1.0f;
		if (g instanceof Graphics2D) {
			Graphics2D g2d = (Graphics2D) g;
			Composite existingComposite = g2d.getComposite();
			if (existingComposite instanceof AlphaComposite) {
				AlphaComposite ac = (AlphaComposite) existingComposite;
				if (ac.getRule() == AlphaComposite.SRC_OVER)
					xFactor = ac.getAlpha();
			}
		}
		float finalAlpha = translucency * xFactor;
		if (c instanceof JComponent) {
			Object alphaObj = ((JComponent) c)
					.getClientProperty(TransitionLayout.ALPHA);
			float transitionAlpha = 1.0f;
			if (alphaObj != null) {
				transitionAlpha = ((Float) alphaObj).floatValue();
			}
			finalAlpha *= transitionAlpha;
		}
		if (finalAlpha == 1.0f)
			return AlphaComposite.SrcOver;
		return AlphaComposite.SrcOver.derive(finalAlpha);
	}

	public static Composite getAlphaComposite(Component c, float translucency) {
		return getAlphaComposite(c, translucency, null);
	}

	/**
	 * Returns the composite to use for painting the specified component. The
	 * result should be set on the {@link Graphics2D} before any custom
	 * rendering is done. This method can be used by application painting code
	 * and by look-and-feel delegates.
	 * 
	 * @param c
	 *            Component.
	 * @return The composite to use for painting the specified component.
	 */
	public static Composite getAlphaComposite(Component c, Graphics g) {
		return getAlphaComposite(c, 1.0f, g);
	}

	/**
	 * Returns the composite to use for painting the specified component. The
	 * result should be set on the {@link Graphics2D} before any custom
	 * rendering is done. This method can be used by application painting code
	 * and by look-and-feel delegates.
	 * 
	 * @param c
	 *            Component.
	 * @return The composite to use for painting the specified component.
	 */
	public static Composite getAlphaComposite(Component c) {
		return getAlphaComposite(c, 1.0f, null);
	}

	/**
	 * Returns indication whether the specified component is opaque. This method
	 * can be used by look-and-feel delegates to decide whether the component
	 * background should be filled. Use in conjunction with
	 * {@link #getAlphaComposite(Component)} or
	 * {@link #getAlphaComposite(Component, float)} to correctly fill the
	 * component background during the transition fade animations. Note that
	 * during the transition fades, the components are marked as non-opaque so
	 * that Swing will handle them correctly. This means that calling
	 * {@link Component#isOpaque()} will return <code>false</code>, incorrectly
	 * signifying to the painting code that the background fill should be
	 * omitted.
	 * 
	 * @param c
	 *            Component.
	 * @return <code>true</code> if the specified component is opaque,
	 *         <code>false</code> otherwise.
	 */
	public static boolean isOpaque(Component c) {
		if (c.isOpaque())
			return true;
		if (c instanceof JComponent) {
			return Boolean.TRUE.equals(((JComponent) c)
					.getClientProperty(ORIGINAL_OPACITY));
		}
		return false;
	}

	void setDoImmediateRepaint(boolean doImmediateRepaint) {
		this.doImmediateRepaint = doImmediateRepaint;
	}

	protected void repaint(Component comp) {
		if (this.doImmediateRepaint && (comp instanceof JComponent)) {
			final JComponent jc = (JComponent) comp;
			SwingUtilities.invokeLater(new Runnable() {
				public void run() {
					jc.paintImmediately(0, 0, jc.getWidth(), jc.getHeight());
				}
			});

			return;
		}
		comp.repaint();
	}

	/**
	 * Returns the composite alpha for the specified component. The same
	 * component can participate in several transitions - a button may be being
	 * hidden while the panel that contains the button is being hidden (switched
	 * out to another tab in a tabbed pane). In this case, the resulting alpha
	 * for the button is multiplication of the alpha of the button fade and the
	 * alpha of the panel fade. This method returns the multiplication of all
	 * the alpha fades of the specified component and its ancestors.
	 * 
	 * @param c
	 *            Component.
	 * @return Composite alpha for the specified component - the multiplication
	 *         of all the alpha fades of the specified component and its
	 *         ancestors
	 */
	protected float getCompositeAlpha(Component c) {
		float result = 1.0f;
		while (c != null) {
			if (c instanceof JComponent) {
				Object alphaObj = ((JComponent) c)
						.getClientProperty(TransitionLayout.OWN_ALPHA);
				float transitionAlpha = 1.0f;
				if (alphaObj != null) {
					transitionAlpha = ((Float) alphaObj).floatValue();
				}
				result *= transitionAlpha;
			}
			c = c.getParent();
		}
		return result;
	}

	public synchronized boolean isAnimating() {
		return (this.getPendingAnimationCount() > 0);
	}

	public synchronized void addTransitionLayoutListener(
			TransitionLayoutListener listener) {
		this.eventListeners.add(listener);
	}

	public synchronized void removeTransitionLayoutListener(
			TransitionLayoutListener listener) {
		this.eventListeners.remove(listener);
	}

	protected void fireEvent(Component child, int id) {
		TransitionLayoutEvent event = new TransitionLayoutEvent(this.container,
				child, id);
		for (Iterator it = this.eventListeners.iterator(); it.hasNext();) {
			TransitionLayoutListener listener = (TransitionLayoutListener) it
					.next();
			listener.onTransitionLayoutEvent(event);
		}
	}
}
