/*******************************************************************************
 * Copyright (c) 2004, 2006 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 - Initial API and implementation
 *******************************************************************************/
package org.eclipse.core.internal.jobs;

import org.eclipse.core.internal.runtime.RuntimeLog;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.core.runtime.jobs.Job;

/**
 * Captures the implicit job state for a given thread. 
 */
class ThreadJob extends Job {
	/**
	 * The notifier is a shared object used to wake up waiting thread jobs
	 * when another job completes that is releasing a scheduling rule.
	 */
	static final Object notifier = new Object();
	
	private final JobManager manager;
	/**
	 * Set to true if this thread job is running in a thread that did
	 * not own a rule already.  This means it needs to acquire the
	 * rule during beginRule, and must release the rule during endRule.
	 */
	protected boolean acquireRule = false;

	/**
	 * Indicates that this thread job did report to the progress manager
	 * that it will be blocked, and therefore when it begins it must
	 * be reported to the job manager when it is no longer blocked.
	 */
	boolean isBlocked = false;

	/**
	 * True if this ThreadJob has begun execution
	 */
	protected boolean isRunning = false;

	/**
	 * Used for diagnosing mismatched begin/end pairs. This field
	 * is only used when in debug mode, to capture the stack trace
	 * of the last call to beginRule.
	 */
	private RuntimeException lastPush = null;
	/**
	 * The actual job that is running in the thread that this 
	 * ThreadJob represents.  This will be null if this thread
	 * job is capturing a rule acquired outside of a job.
	 */
	protected Job realJob;
	/**
	 * The stack of rules that have been begun in this thread, but not yet ended.
	 */
	private ISchedulingRule[] ruleStack;
	/**
	 * Rule stack pointer.
	 */
	private int top;

	ThreadJob(JobManager manager, ISchedulingRule rule) {
		super("Implicit Job"); //$NON-NLS-1$
		this.manager = manager;
		setSystem(true);
		setPriority(Job.INTERACTIVE);
		ruleStack = new ISchedulingRule[2];
		top = -1;
		setRule(rule);
	}

	/**
	 * An endRule was called that did not match the last beginRule in
	 * the stack.  Report and log a detailed informational message.
	 * @param rule The rule that was popped
	 */
	private void illegalPop(ISchedulingRule rule) {
		StringBuffer buf = new StringBuffer("Attempted to endRule: "); //$NON-NLS-1$
		buf.append(rule);
		if (top >= 0 && top < ruleStack.length) {
			buf.append(", does not match most recent begin: "); //$NON-NLS-1$
			buf.append(ruleStack[top]);
		} else {
			if (top < 0)
				buf.append(", but there was no matching beginRule"); //$NON-NLS-1$
			else
				buf.append(", but the rule stack was out of bounds: " + top); //$NON-NLS-1$
		}
		buf.append(".  See log for trace information if rule tracing is enabled."); //$NON-NLS-1$
		String msg = buf.toString();
		if (JobManager.DEBUG || JobManager.DEBUG_BEGIN_END) {
			System.out.println(msg);
			Throwable t = lastPush == null ? new IllegalArgumentException() : lastPush;
			IStatus error = new Status(IStatus.ERROR, JobManager.PI_JOBS, 1, msg, t);
			RuntimeLog.log(error);
		}
		Assert.isLegal(false, msg);
	}

	/**
	 * Client has attempted to begin a rule that is not contained within
	 * the outer rule.
	 */
	private void illegalPush(ISchedulingRule pushRule, ISchedulingRule baseRule) {
		StringBuffer buf = new StringBuffer("Attempted to beginRule: "); //$NON-NLS-1$
		buf.append(pushRule);
		buf.append(", does not match outer scope rule: "); //$NON-NLS-1$
		buf.append(baseRule);
		String msg = buf.toString();
		if (JobManager.DEBUG) {
			System.out.println(msg);
			IStatus error = new Status(IStatus.ERROR, JobManager.PI_JOBS, 1, msg, new IllegalArgumentException());
			RuntimeLog.log(error);
		}
		Assert.isLegal(false, msg);

	}

	/**
	 * Returns true if the monitor is canceled, and false otherwise.
	 * Protects the caller from exception in the monitor implementation.
	 */
	private boolean isCanceled(IProgressMonitor monitor) {
		try {
			return monitor.isCanceled();
		} catch (RuntimeException e) {
			//logged message should not be translated
			IStatus status = new Status(IStatus.ERROR, JobManager.PI_JOBS, JobManager.PLUGIN_ERROR, "ThreadJob.isCanceled", e); //$NON-NLS-1$
			RuntimeLog.log(status);
		}
		return false;
	}

	/**
	 * Returns true if this thread job was scheduled and actually started running.
	 */
	synchronized boolean isRunning() {
		return isRunning;
	}
	
	/**
	 * Schedule the job and block the calling thread until the job starts running.
	 * Returns the ThreadJob instance that was started.
	 */
	ThreadJob joinRun(IProgressMonitor monitor) {
		if (isCanceled(monitor))
			throw new OperationCanceledException();
		//check if there is a blocking thread before waiting
		InternalJob blockingJob = manager.findBlockingJob(this);
		Thread blocker = blockingJob == null ? null : blockingJob.getThread();
		ThreadJob result = this;
		try {
			//just return if lock listener decided to grant immediate access
			if (manager.getLockManager().aboutToWait(blocker))
				return this;
			try {
				waitStart(monitor, blockingJob);
				final Thread currentThread = Thread.currentThread();
				while (true) {
					if (isCanceled(monitor))
						throw new OperationCanceledException();
					//try to run the job 
					if (manager.runNow(this))
						return this;
					//update blocking job
					blockingJob = manager.findBlockingJob(this);
					//the rule could have been transferred to this thread while we were waiting
					blocker = blockingJob == null ? null : blockingJob.getThread();
					if (blocker == currentThread && blockingJob instanceof ThreadJob) {
						//now we are just the nested acquire case
						result = (ThreadJob)blockingJob;
						result.push(getRule());
						result.isBlocked = this.isBlocked;
						return result;
					}
					//just return if lock listener decided to grant immediate access
					if (manager.getLockManager().aboutToWait(blocker))
						return this;
					//must lock instance before calling wait
					synchronized (notifier) {
						try {
							notifier.wait(250);
						} catch (InterruptedException e) {
							//ignore
						}
					}
				}
			} finally {
				if (this == result)
					waitEnd(monitor);
			}
		} finally {
			manager.getLockManager().aboutToRelease();
		}
	}

	/**
	 * Pops a rule. Returns true if it was the last rule for this thread
	 * job, and false otherwise.
	 */
	boolean pop(ISchedulingRule rule) {
		if (top < 0 || ruleStack[top] != rule)
			illegalPop(rule);
		ruleStack[top--] = null;
		return top < 0;
	}

	/**
	 * Adds a new scheduling rule to the stack of rules for this thread. Throws
	 * a runtime exception if the new rule is not compatible with the base
	 * scheduling rule for this thread.
	 */
	void push(final ISchedulingRule rule) {
		final ISchedulingRule baseRule = getRule();
		if (++top >= ruleStack.length) {
			ISchedulingRule[] newStack = new ISchedulingRule[ruleStack.length * 2];
			System.arraycopy(ruleStack, 0, newStack, 0, ruleStack.length);
			ruleStack = newStack;
		}
		ruleStack[top] = rule;
		if (JobManager.DEBUG_BEGIN_END)
			lastPush = (RuntimeException) new RuntimeException().fillInStackTrace();
		//check for containment last because we don't want to fail again on endRule
		if (baseRule != null && rule != null && !baseRule.contains(rule))
			illegalPush(rule, baseRule);
	}

	/**
	 * Reset all of this job's fields so it can be reused.  Returns false if
	 * reuse is not possible
	 */
	boolean recycle() {
		//don't recycle if still running for any reason
		if (getState() != Job.NONE)
			return false;
		//clear and reset all fields
		acquireRule = isRunning = isBlocked = false;
		realJob = null;
		setRule(null);
		setThread(null);
		if (ruleStack.length != 2)
			ruleStack = new ISchedulingRule[2];
		else
			ruleStack[0] = ruleStack[1] = null;
		top = -1;
		return true;
	}

	/** (non-Javadoc)
	 * @see org.eclipse.core.runtime.jobs.Job#run(org.eclipse.core.runtime.IProgressMonitor)
	 */
	public IStatus run(IProgressMonitor monitor) {
		synchronized (this) {
			isRunning = true;
		}
		return ASYNC_FINISH;
	}

	/**
	 * Records the job that is actually running in this thread, if any
	 * @param realJob The running job
	 */
	void setRealJob(Job realJob) {
		this.realJob = realJob;
	}

	/**
	 * Returns true if this job should cause a self-canceling job
	 * to cancel itself, and false otherwise.
	 */
	boolean shouldInterrupt() {
		return realJob == null ? true : !realJob.isSystem();
	}

	/* (non-javadoc)
	 * For debugging purposes only
	 */
	public String toString() {
		StringBuffer buf = new StringBuffer("ThreadJob"); //$NON-NLS-1$
		buf.append('(').append(realJob).append(',').append('[');
		for (int i = 0; i <= top && i < ruleStack.length; i++)
			buf.append(ruleStack[i]).append(',');
		buf.append(']').append(')');
		return buf.toString();
	}

	/**
	 * Reports that this thread was blocked, but is no longer blocked and is able
	 * to proceed.
	 * @param monitor The monitor to report unblocking to.
	 */
	private void waitEnd(IProgressMonitor monitor) {
		final LockManager lockManager = manager.getLockManager();
		final Thread currentThread = Thread.currentThread();
		if (isRunning()) {
			lockManager.addLockThread(currentThread, getRule());
			//need to re-acquire any locks that were suspended while this thread was blocked on the rule
			lockManager.resumeSuspendedLocks(currentThread);
		} else {
			//tell lock manager that this thread gave up waiting
			lockManager.removeLockWaitThread(currentThread, getRule());
		}
	}

	/**
	 * Indicates the start of a wait on a scheduling rule. Report the
	 * blockage to the progress manager and update the lock manager.
	 * @param monitor The monitor to report blocking to
	 * @param blockingJob The job that is blocking this thread, or <code>null</code>
	 */
	private void waitStart(IProgressMonitor monitor, InternalJob blockingJob) {
		manager.getLockManager().addLockWaitThread(Thread.currentThread(), getRule());
		isBlocked = true;
		manager.reportBlocked(monitor, blockingJob);
	}
}