/*
 * Copyright (C) 2008-2011, Google Inc.
 * and other copyright owners as documented in the project's IP log.
 *
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Distribution License v1.0 which
 * accompanies this distribution, is reproduced below, and is
 * available at http://www.eclipse.org/org/documents/edl-v10.php
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - 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.
 *
 * - Neither the name of the Eclipse Foundation, Inc. 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.eclipse.jgit.lib;

import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

/** ProgressMonitor that batches update events. */
public abstract class BatchingProgressMonitor implements ProgressMonitor {
	private static final ScheduledThreadPoolExecutor alarmQueue;

	static final Object alarmQueueKiller;

	static {
		// To support garbage collection, start our thread but
		// swap out the thread factory. When our class is GC'd
		// the alarmQueueKiller will finalize and ask the executor
		// to shutdown, ending the worker.
		//
		int threads = 1;
		alarmQueue = new ScheduledThreadPoolExecutor(threads,
				new ThreadFactory() {
					private final ThreadFactory baseFactory = Executors
							.defaultThreadFactory();

					public Thread newThread(Runnable taskBody) {
						Thread thr = baseFactory.newThread(taskBody);
						thr.setName("JGit-AlarmQueue"); //$NON-NLS-1$
						thr.setDaemon(true);
						return thr;
					}
				});
		alarmQueue.setContinueExistingPeriodicTasksAfterShutdownPolicy(false);
		alarmQueue.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
		alarmQueue.prestartAllCoreThreads();

		// Now that the threads are running, its critical to swap out
		// our own thread factory for one that isn't in the ClassLoader.
		// This allows the class to GC.
		//
		alarmQueue.setThreadFactory(Executors.defaultThreadFactory());

		alarmQueueKiller = new Object() {
			@Override
			protected void finalize() {
				alarmQueue.shutdownNow();
			}
		};
	}

	private long delayStartTime;

	private TimeUnit delayStartUnit = TimeUnit.MILLISECONDS;

	private Task task;

	/**
	 * Set an optional delay before the first output.
	 *
	 * @param time
	 *            how long to wait before output. If 0 output begins on the
	 *            first {@link #update(int)} call.
	 * @param unit
	 *            time unit of {@code time}.
	 */
	public void setDelayStart(long time, TimeUnit unit) {
		delayStartTime = time;
		delayStartUnit = unit;
	}

	public void start(int totalTasks) {
		// Ignore the number of tasks.
	}

	public void beginTask(String title, int work) {
		endTask();
		task = new Task(title, work);
		if (delayStartTime != 0)
			task.delay(delayStartTime, delayStartUnit);
	}

	public void update(int completed) {
		if (task != null)
			task.update(this, completed);
	}

	public void endTask() {
		if (task != null) {
			task.end(this);
			task = null;
		}
	}

	public boolean isCancelled() {
		return false;
	}

	/**
	 * Update the progress monitor if the total work isn't known,
	 *
	 * @param taskName
	 *            name of the task.
	 * @param workCurr
	 *            number of units already completed.
	 */
	protected abstract void onUpdate(String taskName, int workCurr);

	/**
	 * Finish the progress monitor when the total wasn't known in advance.
	 *
	 * @param taskName
	 *            name of the task.
	 * @param workCurr
	 *            total number of units processed.
	 */
	protected abstract void onEndTask(String taskName, int workCurr);

	/**
	 * Update the progress monitor when the total is known in advance.
	 *
	 * @param taskName
	 *            name of the task.
	 * @param workCurr
	 *            number of units already completed.
	 * @param workTotal
	 *            estimated number of units to process.
	 * @param percentDone
	 *            {@code workCurr * 100 / workTotal}.
	 */
	protected abstract void onUpdate(String taskName, int workCurr,
			int workTotal, int percentDone);

	/**
	 * Finish the progress monitor when the total is known in advance.
	 *
	 * @param taskName
	 *            name of the task.
	 * @param workCurr
	 *            total number of units processed.
	 * @param workTotal
	 *            estimated number of units to process.
	 * @param percentDone
	 *            {@code workCurr * 100 / workTotal}.
	 */
	protected abstract void onEndTask(String taskName, int workCurr,
			int workTotal, int percentDone);

	private static class Task implements Runnable {
		/** Title of the current task. */
		private final String taskName;

		/** Number of work units, or {@link ProgressMonitor#UNKNOWN}. */
		private final int totalWork;

		/** True when timer expires and output should occur on next update. */
		private volatile boolean display;

		/** Scheduled timer, supporting cancellation if task ends early. */
		private Future<?> timerFuture;

		/** True if the task has displayed anything. */
		private boolean output;

		/** Number of work units already completed. */
		private int lastWork;

		/** Percentage of {@link #totalWork} that is done. */
		private int lastPercent;

		Task(String taskName, int totalWork) {
			this.taskName = taskName;
			this.totalWork = totalWork;
			this.display = true;
		}

		void delay(long time, TimeUnit unit) {
			display = false;
			timerFuture = alarmQueue.schedule(this, time, unit);
		}

		public void run() {
			display = true;
		}

		void update(BatchingProgressMonitor pm, int completed) {
			lastWork += completed;

			if (totalWork == UNKNOWN) {
				// Only display once per second, as the alarm fires.
				if (display) {
					pm.onUpdate(taskName, lastWork);
					output = true;
					restartTimer();
				}
			} else {
				// Display once per second or when 1% is done.
				int currPercent = lastWork * 100 / totalWork;
				if (display) {
					pm.onUpdate(taskName, lastWork, totalWork, currPercent);
					output = true;
					restartTimer();
					lastPercent = currPercent;
				} else if (currPercent != lastPercent) {
					pm.onUpdate(taskName, lastWork, totalWork, currPercent);
					output = true;
					lastPercent = currPercent;
				}
			}
		}

		private void restartTimer() {
			display = false;
			timerFuture = alarmQueue.schedule(this, 1, TimeUnit.SECONDS);
		}

		void end(BatchingProgressMonitor pm) {
			if (output) {
				if (totalWork == UNKNOWN) {
					pm.onEndTask(taskName, lastWork);
				} else {
					int pDone = lastWork * 100 / totalWork;
					pm.onEndTask(taskName, lastWork, totalWork, pDone);
				}
			}
			if (timerFuture != null)
				timerFuture.cancel(false /* no interrupt */);
		}
	}
}
