/*******************************************************************************
 * Copyright (c) 2000, 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
 *
 
 *******************************************************************************/
package org.eclipse.dltk.internal.core.search.processing;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.dltk.core.DLTKCore;
import org.eclipse.dltk.internal.core.util.Messages;
import org.eclipse.dltk.internal.core.util.Util;

public abstract class JobManager implements Runnable {

	/* queue of jobs to execute */
	protected IJob[] awaitingJobs = new IJob[10];
	protected int jobStart = 0;
	protected int jobEnd = -1;
	protected boolean executing = false;

	/* background processing */
	protected Thread processingThread;
	protected Job progressJob;

	/*
	 * counter indicating whether job execution is enabled or not, disabled if
	 * <= 0 it cannot go beyond 1
	 */
	private int enableCount = 1;

	public final static boolean VERBOSE = DLTKCore.VERBOSE_JOBMANAGER;
	/* flag indicating that the activation has completed */
	public boolean activated = false;

	private int awaitingClients = 0;

	/**
	 * Invoked exactly once, in background, before starting processing any job
	 */
	public void activateProcessing() {
		this.activated = true;
	}

	/**
	 * Answer the amount of awaiting jobs.
	 */
	public synchronized int awaitingJobsCount() {
		// pretend busy in case concurrent job attempts performing before
		// activated
		return this.activated ? this.jobEnd - this.jobStart + 1 : 1;
	}

	/**
	 * Answers the first job in the queue, or null if there is no job available
	 * Until the job has completed, the job manager will keep answering the same
	 * job.
	 */
	public synchronized IJob currentJob() {
		if (this.enableCount > 0 && this.jobStart <= this.jobEnd)
			return this.awaitingJobs[this.jobStart];
		return null;
	}

	public void disable() {
		this.enableCount--;
		if (VERBOSE)
			Util.verbose("DISABLING background indexing"); //$NON-NLS-1$
	}

	/**
	 * Remove the index from cache for a given project. Passing null as a job
	 * family discards them all.
	 */
	public void discardJobs(String jobFamily) {

		if (VERBOSE)
			Util.verbose("DISCARD   background job family - " + jobFamily); //$NON-NLS-1$

		try {
			IJob currentJob;
			// cancel current job if it belongs to the given family
			synchronized (this) {
				currentJob = this.currentJob();
				disable();
			}
			if (currentJob != null
					&& (jobFamily == null || currentJob.belongsTo(jobFamily))) {
				currentJob.cancel();

				// wait until current active job has finished
				while (this.processingThread != null && this.executing) {
					try {
						if (VERBOSE)
							Util
									.verbose("-> waiting end of current background job - " + currentJob); //$NON-NLS-1$
						Thread.sleep(50);
					} catch (InterruptedException e) {
						// ignore
					}
				}
			}

			// flush and compact awaiting jobs
			int loc = -1;
			synchronized (this) {
				for (int i = this.jobStart; i <= this.jobEnd; i++) {
					currentJob = this.awaitingJobs[i];
					if (currentJob != null) { // sanity check
						this.awaitingJobs[i] = null;
						if (!(jobFamily == null || currentJob
								.belongsTo(jobFamily))) { // copy down,
							// compacting
							this.awaitingJobs[++loc] = currentJob;
						} else {
							if (VERBOSE)
								Util
										.verbose("-> discarding background job  - " + currentJob); //$NON-NLS-1$
							currentJob.cancel();
						}
					}
				}
				this.jobStart = 0;
				this.jobEnd = loc;
			}
		} finally {
			enable();
		}
		if (VERBOSE)
			Util
					.verbose("DISCARD   DONE with background job family - " + jobFamily); //$NON-NLS-1$
	}

	public synchronized void enable() {
		this.enableCount++;
		if (VERBOSE)
			Util.verbose("ENABLING  background indexing"); //$NON-NLS-1$
		this.notifyAll(); // wake up the background thread if it is waiting
		// (context must be synchronized)
	}

	protected synchronized boolean isJobWaiting(IJob request) {
		for (int i = this.jobEnd; i > this.jobStart; i--)
			// don't check job at jobStart, as it may have already started
			if (request.equals(this.awaitingJobs[i]))
				return true;
		return false;
	}

	/**
	 * Advance to the next available job, once the current one has been
	 * completed. Note: clients awaiting until the job count is zero are still
	 * waiting at this point.
	 */
	protected synchronized void moveToNextJob() {
		// if (!enabled) return;

		if (this.jobStart <= this.jobEnd) {
			this.awaitingJobs[this.jobStart++] = null;
			if (this.jobStart > this.jobEnd) {
				this.jobStart = 0;
				this.jobEnd = -1;
			}
		}
	}

	/**
	 * When idle, give chance to do something
	 */
	protected void notifyIdle(long idlingTime) {
		// do nothing
	}

	/**
	 * This API is allowing to run one job in concurrence with background
	 * processing. Indeed since other jobs are performed in background, resource
	 * sharing might be an issue.Therefore, this functionality allows a given
	 * job to be run without colliding with background ones. Note: multiple
	 * thread might attempt to perform concurrent jobs at the same time, and
	 * should synchronize (it is deliberately left to clients to decide whether
	 * concurrent jobs might interfere or not. In general, multiple read jobs
	 * are ok).
	 * 
	 * Waiting policy can be: IJobConstants.ForceImmediateSearch
	 * IJobConstants.CancelIfNotReadyToSearch
	 * IJobConstants.WaitUntilReadyToSearch
	 * 
	 */
	public boolean performConcurrentJob(IJob searchJob, int waitingPolicy,
			IProgressMonitor progress) {
		if (VERBOSE)
			Util.verbose("STARTING  concurrent job - " + searchJob); //$NON-NLS-1$

		searchJob.ensureReadyToRun();

		int concurrentJobWork = 100;
		if (progress != null)
			progress.beginTask("", concurrentJobWork); //$NON-NLS-1$
		boolean status = IJob.FAILED;
		if (awaitingJobsCount() > 0) {
			switch (waitingPolicy) {

			case IJob.ForceImmediate:
				if (VERBOSE)
					Util
							.verbose("-> NOT READY - forcing immediate - " + searchJob);//$NON-NLS-1$
				try {
					disable(); // pause indexing
					status = searchJob.execute(progress == null ? null
							: new SubProgressMonitor(progress,
									concurrentJobWork));
				} finally {
					enable();
				}
				if (VERBOSE)
					Util.verbose("FINISHED  concurrent job - " + searchJob); //$NON-NLS-1$
				return status;

			case IJob.CancelIfNotReady:
				if (VERBOSE)
					Util.verbose("-> NOT READY - cancelling - " + searchJob); //$NON-NLS-1$
				if (VERBOSE)
					Util.verbose("CANCELED concurrent job - " + searchJob); //$NON-NLS-1$
				throw new OperationCanceledException();

			case IJob.WaitUntilReady:
				int awaitingWork;
				IJob previousJob = null;
				IJob currentJob;
				IProgressMonitor subProgress = null;
				int totalWork = this.awaitingJobsCount();
				if (progress != null && totalWork > 0) {
					subProgress = new SubProgressMonitor(progress,
							concurrentJobWork / 2);
					subProgress.beginTask("", totalWork); //$NON-NLS-1$
					concurrentJobWork = concurrentJobWork / 2;
				}
				// use local variable to avoid potential NPE (see bug 20435 NPE
				// when searchingscriptmethod
				// and bug 42760 NullPointerException in JobManager when
				// searching)
				Thread t = this.processingThread;
				int originalPriority = t == null ? -1 : t.getPriority();
				try {
					if (t != null)
						t.setPriority(Thread.currentThread().getPriority());
					synchronized (this) {
						this.awaitingClients++;
					}
					while ((awaitingWork = awaitingJobsCount()) > 0) {
						if (subProgress != null && subProgress.isCanceled())
							throw new OperationCanceledException();
						currentJob = currentJob();
						// currentJob can be null when jobs have been added to
						// the queue but job manager is not enabled
						if (currentJob != null && currentJob != previousJob) {
							if (VERBOSE)
								Util
										.verbose("-> NOT READY - waiting until ready - " + searchJob);//$NON-NLS-1$
							if (subProgress != null) {
								subProgress.subTask(Messages.bind(
										Messages.manager_filesToIndex, Integer
												.toString(awaitingWork)));
								subProgress.worked(1);
							}
							previousJob = currentJob;
						}
						try {
							if (VERBOSE)
								Util
										.verbose("-> GOING TO SLEEP - " + searchJob);//$NON-NLS-1$
							Thread.sleep(50);
						} catch (InterruptedException e) {
							// ignore
						}
					}
				} finally {
					synchronized (this) {
						this.awaitingClients--;
					}
					if (t != null && originalPriority > -1 && t.isAlive())
						t.setPriority(originalPriority);
				}
				if (subProgress != null)
					subProgress.done();
			}
		}
		status = searchJob.execute(progress == null ? null
				: new SubProgressMonitor(progress, concurrentJobWork));
		if (progress != null)
			progress.done();
		if (VERBOSE)
			Util.verbose("FINISHED  concurrent job - " + searchJob); //$NON-NLS-1$
		return status;
	}

	public abstract String processName();

	public synchronized void request(IJob job) {

		job.ensureReadyToRun();

		// append the job to the list of ones to process later on
		int size = this.awaitingJobs.length;
		if (++this.jobEnd == size) { // when growing, relocate jobs starting at
			// position 0
			this.jobEnd -= this.jobStart;
			System.arraycopy(this.awaitingJobs, this.jobStart,
					this.awaitingJobs = new IJob[size * 2], 0, this.jobEnd);
			this.jobStart = 0;
		}
		this.awaitingJobs[this.jobEnd] = job;
		if (VERBOSE) {
			Util.verbose("REQUEST   background job - " + job); //$NON-NLS-1$
			Util.verbose("AWAITING JOBS count: " + awaitingJobsCount()); //$NON-NLS-1$
		}
		notifyAll(); // wake up the background thread if it is waiting
	}

	/**
	 * Flush current state
	 */
	public synchronized void reset() {
		if (VERBOSE)
			Util.verbose("Reset"); //$NON-NLS-1$

		if (this.processingThread != null) {
			discardJobs(null); // discard all jobs
		} else {
			/* initiate background processing */
			this.processingThread = new Thread(this, this.processName());
			this.processingThread.setDaemon(true);
			// less prioritary by default, priority is raised if clients are
			// actively waiting on it
			this.processingThread.setPriority(Thread.NORM_PRIORITY - 1);
			this.processingThread.start();
		}
	}

	/**
	 * Infinite loop performing resource indexing
	 */
	public void run() {

		long idlingStart = -1;
		activateProcessing();
		try {
			class ProgressJob extends Job {
				ProgressJob(String name) {
					super(name);
				}

				protected IStatus run(IProgressMonitor monitor) {
					int awaitingJobsCount;
					while (!monitor.isCanceled()
							&& (awaitingJobsCount = awaitingJobsCount()) > 0) {
						monitor.subTask(Messages.bind(
								Messages.manager_filesToIndex, Integer
										.toString(awaitingJobsCount)));
						try {
							Thread.sleep(500);
						} catch (InterruptedException e) {
							// ignore
						}
					}
					return Status.OK_STATUS;
				}
			}
			this.progressJob = null;
			while (this.processingThread != null) {
				try {
					IJob job;
					synchronized (this) {
						// handle shutdown case when notifyAll came before the
						// wait but after the while loop was entered
						if (this.processingThread == null)
							continue;

						// must check for new job inside this sync block to
						// avoid timing hole
						if ((job = currentJob()) == null) {
							if (this.progressJob != null) {
								this.progressJob.cancel();
								this.progressJob = null;
							}
							if (idlingStart < 0)
								idlingStart = System.currentTimeMillis();
							else
								notifyIdle(System.currentTimeMillis()
										- idlingStart);
							this.wait(); // wait until a new job is posted (or
							// reenabled:38901)
						} else {
							idlingStart = -1;
						}
					}
					if (job == null) {
						notifyIdle(System.currentTimeMillis() - idlingStart);
						// just woke up, delay before processing any new jobs,
						// allow some time for the active thread to finish
						Thread.sleep(500);
						continue;
					}
					if (VERBOSE) {
						Util.verbose(awaitingJobsCount() + " awaiting jobs"); //$NON-NLS-1$
						Util.verbose("STARTING background job - " + job); //$NON-NLS-1$
					}
					try {
						this.executing = true;
						if (this.progressJob == null) {
							this.progressJob = new ProgressJob(
									Messages.manager_indexingInProgress);
							this.progressJob.setPriority(Job.LONG);
							// this.progressJob.setSystem(true);
							this.progressJob.schedule();
						}
						/* boolean status = */job.execute(null);
						// if (status == FAILED) request(job);
					} finally {
						this.executing = false;
						if (VERBOSE)
							Util.verbose("FINISHED background job - " + job); //$NON-NLS-1$
						moveToNextJob();
						// if (this.awaitingClients == 0)
						// Thread.sleep(50);
					}
				} catch (InterruptedException e) { // background indexing was
					// interrupted
				}
			}
		} catch (RuntimeException e) {
			if (this.processingThread != null) { // if not shutting down
				// log exception
				Util.log(e, "Background Indexer Crash Recovery"); //$NON-NLS-1$

				// keep job manager alive
				this.discardJobs(null);
				this.processingThread = null;
				this.reset(); // this will fork a new thread with no waiting
				// jobs, some indexes will be inconsistent
			}
			throw e;
		} catch (Error e) {
			if (this.processingThread != null && !(e instanceof ThreadDeath)) {
				// log exception
				Util.log(e, "Background Indexer Crash Recovery"); //$NON-NLS-1$

				// keep job manager alive
				this.discardJobs(null);
				this.processingThread = null;
				this.reset(); // this will fork a new thread with no waiting
				// jobs, some indexes will be inconsistent
			}
			throw e;
		}
	}

	/**
	 * Stop background processing, and wait until the current job is completed
	 * before returning
	 */
	public void shutdown() {

		if (VERBOSE)
			Util.verbose("Shutdown"); //$NON-NLS-1$

		disable();
		discardJobs(null); // will wait until current executing job has
		// completed
		Thread thread = this.processingThread;
		try {
			if (thread != null) { // see
				// http://bugs.eclipse.org/bugs/show_bug.cgi
				// ?id=31858
				synchronized (this) {
					this.processingThread = null; // mark the job manager as
					// shutting down so that the
					// thread will stop by
					// itself
					this.notifyAll(); // ensure its awake so it can be shutdown
				}
				// in case processing thread is handling a job
				thread.join();
			}
			Job job = this.progressJob;
			if (job != null) {
				job.cancel();
				job.join();
			}
		} catch (InterruptedException e) {
			// ignore
		}
	}

	public String toString() {
		StringBuffer buffer = new StringBuffer(10);
		buffer.append("Enable count:").append(this.enableCount).append('\n'); //$NON-NLS-1$
		int numJobs = this.jobEnd - this.jobStart + 1;
		buffer.append("Jobs in queue:").append(numJobs).append('\n'); //$NON-NLS-1$
		for (int i = 0; i < numJobs && i < 15; i++) {
			buffer
					.append(i)
					.append(" - job[" + i + "]: ").append(this.awaitingJobs[this.jobStart + i]).append('\n'); //$NON-NLS-1$ //$NON-NLS-2$
		}
		return buffer.toString();
	}
}
