/*******************************************************************************
 * Copyright (c) 2006, 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.compare.internal;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;

import org.eclipse.core.runtime.*;
import org.eclipse.jface.operation.IRunnableWithProgress;

/**
 * A worker performs a set of tasks in order and accumulates any errors
 * that may have occurred. If the same task is queued multiple times,
 * the last occurrence will be run. If a task is queued while it is
 * running, the running task will be canceled and the task added
 * to the end of the work queue.
 */
public class Worker implements IRunnableWithProgress {

	private final WorkQueue work = new WorkQueue();
	private boolean isWorking;
	private final List errors = new ArrayList();
	private WorkProgressMonitor currentMonitor;
	private IRunnableWithProgress currentTask;
	private final String taskName;
	
	/**
	 * Progress monitor that supports local cancelation of a task.
	 */
	private static class WorkProgressMonitor extends ProgressMonitorWrapper {
		private boolean localCancel;
		protected WorkProgressMonitor(IProgressMonitor monitor) {
			super(monitor);
		}
		public void cancelTask() {
			localCancel = true;
		}
		public boolean isCanceled() {
			return localCancel || super.isCanceled();
		}
	}
	
	public Worker(String taskName) {
		this.taskName = taskName;
	}
	
	public void run(IProgressMonitor monitor) {
		errors.clear();
		SubMonitor pm = SubMonitor.convert(monitor, getTaskName(), 100);
		try {
			isWorking = true;
			while (!work.isEmpty()) {
				try {
					performNextTask(pm);
					checkCancelled(pm);
				} catch (OperationCanceledException e) {
					// Only cancel all the work if the outer monitor is canceled
					checkCancelled(pm);
				} catch (InterruptedException e) {
					// Only cancel all the work if the outer monitor is canceled
					checkCancelled(pm);
				} catch (InvocationTargetException e) {
					handleError(e.getTargetException());
				}
				pm.setWorkRemaining(100);
			}
		} catch (OperationCanceledException e) {
			// The user chose to cancel
			work.clear();
		} finally {
			isWorking = false;
			if (monitor!= null)
				monitor.done();
			currentMonitor = null;
			currentTask = null;
		}
	}

	private WorkProgressMonitor subMonitorFor(SubMonitor pm, int ticks) {
		return new WorkProgressMonitor(pm.newChild(ticks));
	}

	private void handleError(Throwable targetException) {
		errors.add(targetException);
	}
	
	public Throwable[] getErrors() {
		return (Throwable[]) errors.toArray(new Throwable[errors.size()]);
	}

	private void checkCancelled(SubMonitor pm) {
		if (pm.isCanceled())
			throw new OperationCanceledException();
	}

	protected String getTaskName() {
		return taskName;
	}

	private void performNextTask(SubMonitor pm) throws InvocationTargetException, InterruptedException {
		synchronized (this) {
			if (work.isEmpty())
				return;
			currentTask = work.remove();
		}
		currentMonitor = subMonitorFor(pm, 10);
		currentTask.run(currentMonitor);
	}

	public synchronized void add(IRunnableWithProgress r) {
		if (currentTask != null && currentTask.equals(r)) {
			currentMonitor.cancelTask();
		}
		work.add(r);
	}

	public boolean isWorking() {
		return isWorking;
	}

	public boolean hasWork() {
		return isWorking() || !work.isEmpty();
	}

}
