/*******************************************************************************
 * Copyright (c) 2000, 2010 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.draw2d;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.swt.SWTException;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.widgets.Display;

import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.swt.SWT;

/**
 * An UpdateManager that asynchronously updates the affected figures.
 */
public class DeferredUpdateManager extends UpdateManager {

	/**
	 * Calls {@link DeferredUpdateManager#performUpdate()}.
	 */
	protected class UpdateRequest implements Runnable {

		public UpdateRequest() {
			super();
		}

		/**
		 * Calls {@link DeferredUpdateManager#performUpdate()}.
		 */
		public void run() {
			performUpdate();
		}
	}

	private Rectangle damage;
	private Map dirtyRegions = new HashMap();

	private GraphicsSource graphicsSource;
	private List invalidFigures = new ArrayList();
	private IFigure root;
	private boolean updateQueued;

	private boolean updating;
	private boolean validating;
	private RunnableChain afterUpdate;

	private static class RunnableChain {
		RunnableChain next;
		Runnable run;

		RunnableChain(Runnable run, RunnableChain next) {
			this.run = run;
			this.next = next;
		}

		void run() {
			if (next != null)
				next.run();
			run.run();
		}
	}

	/**
	 * Empty constructor.
	 */
	public DeferredUpdateManager() {
	}

	/**
	 * Constructs a new DererredUpdateManager with the given GraphicsSource.
	 * 
	 * @param gs
	 *            the graphics source
	 */
	public DeferredUpdateManager(GraphicsSource gs) {
		setGraphicsSource(gs);
	}

	/**
	 * Adds a dirty region (defined by the rectangle <i>x, y, w, h</i>) to the
	 * update queue. If the figure isn't visible or either the width or height
	 * are 0, the method returns without queueing the dirty region.
	 * 
	 * @param figure
	 *            the figure that contains the dirty region
	 * @param x
	 *            the x coordinate of the dirty region
	 * @param y
	 *            the y coordinate of the dirty region
	 * @param w
	 *            the width of the dirty region
	 * @param h
	 *            the height of the dirty region
	 */
	public synchronized void addDirtyRegion(IFigure figure, int x, int y,
			int w, int h) {
		if (w == 0 || h == 0 || !figure.isShowing())
			return;

		Rectangle rect = (Rectangle) dirtyRegions.get(figure);
		if (rect == null) {
			rect = new Rectangle(x, y, w, h);
			dirtyRegions.put(figure, rect);
		} else
			rect.union(x, y, w, h);

		queueWork();
	}

	/**
	 * Adds the given figure to the update queue. Invalid figures will be
	 * validated before the damaged regions are repainted.
	 * 
	 * @param f
	 *            the invalid figure
	 */
	public synchronized void addInvalidFigure(IFigure f) {
		if (invalidFigures.contains(f))
			return;
		queueWork();
		invalidFigures.add(f);
	}

	/**
	 * Returns a Graphics object for the given region.
	 * 
	 * @param region
	 *            the region to be repainted
	 * @return the Graphics object
	 */
	protected Graphics getGraphics(Rectangle region) {
		if (graphicsSource == null)
			return null;
		return graphicsSource.getGraphics(region);
	}

	void paint(GC gc) {
		if (!validating) {
			SWTGraphics graphics = new SWTGraphics(gc);
			if (!updating) {
				/**
				 * If a paint occurs not as part of an update, we should notify
				 * that the region is being painted. Otherwise, notification
				 * already occurs in repairDamage().
				 */
				Rectangle rect = graphics.getClip(new Rectangle());
				HashMap map = new HashMap();
				map.put(root, rect);
				firePainting(rect, map);
			}
			performValidation();
			root.paint(graphics);
			graphics.dispose();
		} else {
			/*
			 * If figures are being validated then we can simply add a dirty
			 * region here and update will repaint this region with other dirty
			 * regions when it gets to painting. We can't paint if we're not
			 * sure that all figures are valid.
			 */
			addDirtyRegion(root, new Rectangle(gc.getClipping()));
		}
	}

	/**
	 * Performs the update. Validates the invalid figures and then repaints the
	 * dirty regions.
	 * 
	 * @see #validateFigures()
	 * @see #repairDamage()
	 */
	public synchronized void performUpdate() {
		if (isDisposed() || updating)
			return;
		updating = true;
		try {
			performValidation();
			updateQueued = false;
			repairDamage();
			if (afterUpdate != null) {
				RunnableChain chain = afterUpdate;
				afterUpdate = null;
				chain.run(); // chain may queue additional Runnable.
				if (afterUpdate != null)
					queueWork();
			}
		} finally {
			updating = false;
		}
	}

	/**
	 * @see UpdateManager#performValidation()
	 */
	public synchronized void performValidation() {
		if (invalidFigures.isEmpty() || validating)
			return;
		try {
			IFigure fig;
			validating = true;
			fireValidating();
			for (int i = 0; i < invalidFigures.size(); i++) {
				fig = (IFigure) invalidFigures.get(i);
				invalidFigures.set(i, null);
				fig.validate();
			}
		} finally {
			invalidFigures.clear();
			validating = false;
		}
	}

	/**
	 * Adds the given exposed region to the update queue and then performs the
	 * update.
	 * 
	 * @param exposed
	 *            the exposed region
	 */
	public synchronized void performUpdate(Rectangle exposed) {
		addDirtyRegion(root, exposed);
		performUpdate();
	}

	/**
	 * Posts an {@link UpdateRequest} using {@link Display#asyncExec(Runnable)}.
	 * If work has already been queued, a new request is not needed.
	 */
	protected void queueWork() {
		if (!updateQueued) {
			sendUpdateRequest();
			updateQueued = true;
		}
	}

	/**
	 * Fires the <code>UpdateRequest</code> to the current display
	 * asynchronously.
	 * 
	 * @since 3.2
	 */
	protected void sendUpdateRequest() {
		Display display = Display.getCurrent();
		if (display == null) {
			throw new SWTException(SWT.ERROR_THREAD_INVALID_ACCESS);
		}
		display.asyncExec(new UpdateRequest());
	}

	/**
	 * Releases the graphics object, which causes the GraphicsSource to flush.
	 * 
	 * @param graphics
	 *            the graphics object
	 */
	protected void releaseGraphics(Graphics graphics) {
		graphics.dispose();
		graphicsSource.flushGraphics(damage);
	}

	/**
	 * Repaints the dirty regions on the update queue and calls
	 * {@link UpdateManager#firePainting(Rectangle, Map)}, unless there are no
	 * dirty regions.
	 */
	protected void repairDamage() {
		Iterator keys = dirtyRegions.keySet().iterator();
		Rectangle contribution;
		IFigure figure;
		IFigure walker;

		while (keys.hasNext()) {
			figure = (IFigure) keys.next();
			walker = figure.getParent();
			contribution = (Rectangle) dirtyRegions.get(figure);
			// A figure can't paint beyond its own bounds
			contribution.intersect(figure.getBounds());
			while (!contribution.isEmpty() && walker != null) {
				walker.translateToParent(contribution);
				contribution.intersect(walker.getBounds());
				walker = walker.getParent();
			}
			if (damage == null)
				damage = new Rectangle(contribution);
			else
				damage.union(contribution);
		}

		if (!dirtyRegions.isEmpty()) {
			Map oldRegions = dirtyRegions;
			dirtyRegions = new HashMap();
			firePainting(damage, oldRegions);
		}

		if (damage != null && !damage.isEmpty()) {
			// ystem.out.println(damage);
			Graphics graphics = getGraphics(damage);
			if (graphics != null) {
				root.paint(graphics);
				releaseGraphics(graphics);
			}
		}
		damage = null;
	}

	/**
	 * Adds the given runnable and queues an update if an update is not under
	 * progress.
	 * 
	 * @param runnable
	 *            the runnable
	 */
	public synchronized void runWithUpdate(Runnable runnable) {
		afterUpdate = new RunnableChain(runnable, afterUpdate);
		if (!updating)
			queueWork();
	}

	/**
	 * Sets the graphics source.
	 * 
	 * @param gs
	 *            the graphics source
	 */
	public void setGraphicsSource(GraphicsSource gs) {
		graphicsSource = gs;
	}

	/**
	 * Sets the root figure.
	 * 
	 * @param figure
	 *            the root figure
	 */
	public void setRoot(IFigure figure) {
		root = figure;
	}

	/**
	 * Validates all invalid figures on the update queue and calls
	 * {@link UpdateManager#fireValidating()} unless there are no invalid
	 * figures.
	 */
	protected void validateFigures() {
		performValidation();
	}

}
