/*******************************************************************************
 * Copyright (c) 2003, 2008 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
 *     Warren Paul (Nokia) - Fix for build scheduling bug 209236
 *******************************************************************************/
package org.eclipse.core.internal.events;

import org.eclipse.core.internal.resources.ResourceException;
import org.eclipse.core.internal.resources.Workspace;
import org.eclipse.core.internal.utils.Messages;
import org.eclipse.core.internal.utils.Policy;
import org.eclipse.core.resources.*;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.Preferences.PropertyChangeEvent;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.core.runtime.jobs.Job;
import org.osgi.framework.Bundle;

/**
 * The job for performing workspace auto-builds, and pre- and post- autobuild
 * notification.  This job is run whenever the workspace changes regardless 
 * of whether autobuild is on or off.
 */
class AutoBuildJob extends Job implements Preferences.IPropertyChangeListener {
	private boolean avoidBuild = false;
	private boolean buildNeeded = false;
	private boolean forceBuild = false;
	/**
	 * Indicates that another thread tried to modify the workspace during
	 * the autobuild.  The autobuild should be immediately rescheduled
	 * so that it will run as soon as the next workspace modification completes.
	 */
	private boolean interrupted = false;
	private boolean isAutoBuilding = false;
	private long lastBuild = 0L;
	private Preferences preferences = ResourcesPlugin.getPlugin().getPluginPreferences();
	private final Bundle systemBundle = Platform.getBundle("org.eclipse.osgi"); //$NON-NLS-1$
	private Workspace workspace;

	AutoBuildJob(Workspace workspace) {
		super(Messages.events_building_0);
		setRule(workspace.getRoot());
		setPriority(BUILD);
		isAutoBuilding = workspace.isAutoBuilding();
		this.workspace = workspace;
		this.preferences.addPropertyChangeListener(this);
	}

	/**
	 * Used to prevent auto-builds at the end of operations that contain
	 * explicit builds
	 */
	synchronized void avoidBuild() {
		avoidBuild = true;
	}

	public boolean belongsTo(Object family) {
		return family == ResourcesPlugin.FAMILY_AUTO_BUILD;
	}

	/**
	 * Instructs the build job that a build is required.  Ensure the build
	 * job is scheduled to run.
	 * @param needsBuild Whether a build is required, either due to 
	 * workspace change or other factor that invalidates the built state.
	 */
	synchronized void build(boolean needsBuild) {
		buildNeeded |= needsBuild;
		long delay = computeScheduleDelay();
		int state = getState();
		if (Policy.DEBUG_BUILD_NEEDED)
			Policy.debug("Build requested, needsBuild: " + needsBuild + " state: " + state + " delay: " + delay); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		if (needsBuild && Policy.DEBUG_BUILD_NEEDED_STACK && state != Job.RUNNING)
			new RuntimeException("Build Needed").printStackTrace(); //$NON-NLS-1$
		//don't mess with the interrupt flag if the job is still running
		if (state != Job.RUNNING)
			setInterrupted(false);
		switch (state) {
			case Job.SLEEPING :
				wakeUp(delay);
				break;
			case NONE :
				setSystem(!isAutoBuilding);
				schedule(delay);
				break;
		}
	}

	/**
	 * Computes the delay time that autobuild should be scheduled with.  The
	 * value will be in the range (MIN_BUILD_DELAY, MAX_BUILD_DELAY).
	 */
	private long computeScheduleDelay() {
		// don't assume that the last build time is always less than the current system time
		long maxDelay = Math.min(Policy.MAX_BUILD_DELAY, Policy.MAX_BUILD_DELAY + lastBuild - System.currentTimeMillis());
		return Math.max(Policy.MIN_BUILD_DELAY, maxDelay);
	}

	/**
	 * The autobuild job has been canceled.  There are two flavours of
	 * cancel, explicit user cancelation, and implicit interruption due to another
	 * thread trying to modify the workspace.  In the latter case, we must 
	 * make sure the build is immediately rescheduled if it was interrupted 
	 * by another thread, so that clients waiting to join autobuild will properly 
	 * continue waiting
	 * @return a status with severity <code>CANCEL</code>
	 */
	private synchronized IStatus canceled() {
		//regardless of the form of cancelation, the build state is not happy
		buildNeeded = true;
		//schedule a rebuild immediately if build was implicitly canceled
		if (interrupted) {
			if (Policy.DEBUG_BUILD_INTERRUPT)
				System.out.println("Scheduling rebuild due to interruption"); //$NON-NLS-1$
			setInterrupted(false);
			schedule(computeScheduleDelay());
		}
		return Status.CANCEL_STATUS;
	}

	private void doBuild(IProgressMonitor monitor) throws CoreException, OperationCanceledException {
		monitor = Policy.monitorFor(monitor);
		try {
			monitor.beginTask("", Policy.opWork); //$NON-NLS-1$
			final ISchedulingRule rule = workspace.getRuleFactory().buildRule();
			try {
				workspace.prepareOperation(rule, monitor);
				workspace.beginOperation(true);
				final int trigger = IncrementalProjectBuilder.AUTO_BUILD;
				workspace.broadcastBuildEvent(workspace, IResourceChangeEvent.PRE_BUILD, trigger);
				IStatus result = Status.OK_STATUS;
				try {
					if (shouldBuild())
						result = workspace.getBuildManager().build(trigger, Policy.subMonitorFor(monitor, Policy.opWork));
				} finally {
					//always send POST_BUILD if there has been a PRE_BUILD
					workspace.broadcastBuildEvent(workspace, IResourceChangeEvent.POST_BUILD, trigger);
				}
				if (!result.isOK())
					throw new ResourceException(result);
				buildNeeded = false;
			} finally {
				//building may close the tree, but we are still inside an
				// operation so open it
				if (workspace.getElementTree().isImmutable())
					workspace.newWorkingTree();
				workspace.endOperation(rule, false, Policy.subMonitorFor(monitor, Policy.endOpWork));
			}
		} finally {
			monitor.done();
		}
	}
	
	/**
	 * Forces an autobuild to occur, even if nothing has changed since the last
	 * build. This is used to force a build after a clean.
	 */
	public void forceBuild() {
		forceBuild = true;
	}

	/**
	 * Another thread is attempting to modify the workspace. Flag the auto-build
	 * as interrupted so that it will cancel and reschedule itself
	 */
	synchronized void interrupt() {
		//if already interrupted, do nothing
		if (interrupted)
			return;
		switch (getState()) {
			case NONE :
				return;
			case WAITING :
				//put the job to sleep if it is waiting to run
				setInterrupted(!sleep());
				break;
			case RUNNING :
				//make sure autobuild doesn't interrupt itself
				if (Job.getJobManager().currentJob() == this)
					return;
				setInterrupted(true);
				if (interrupted && Policy.DEBUG_BUILD_INTERRUPT) {
					System.out.println("Autobuild was interrupted:"); //$NON-NLS-1$
					new Exception().fillInStackTrace().printStackTrace();
				}
				break;
		}
		//clear the autobuild avoidance flag if we were interrupted
		if (interrupted)
			avoidBuild = false;
	}

	synchronized boolean isInterrupted() {
		if (interrupted)
			return true;
		//check if another job is blocked by the build job
		if (isBlocking())
			setInterrupted(true);
		return interrupted;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.core.runtime.Preferences.IPropertyChangeListener#propertyChange(org.eclipse.core.runtime.Preferences.PropertyChangeEvent)
	 */
	public void propertyChange(PropertyChangeEvent event) {
		if (!event.getProperty().equals(ResourcesPlugin.PREF_AUTO_BUILDING))
			return;
		// get the new value of auto-build directly from the preferences
		boolean wasAutoBuilding = isAutoBuilding;
		isAutoBuilding = preferences.getBoolean(ResourcesPlugin.PREF_AUTO_BUILDING);
		//force a build if autobuild has been turned on
		if (!forceBuild && !wasAutoBuilding && isAutoBuilding) {
			forceBuild = true;
			build(false);
		}
	}

	/* (non-Javadoc)
	 * @see org.eclipse.core.internal.jobs.InternalJob#run(org.eclipse.core.runtime.IProgressMonitor)
	 */
	public IStatus run(IProgressMonitor monitor) {
		//synchronized in case build starts during checkCancel
		synchronized (this) {
			if (monitor.isCanceled()) {
				return canceled();
			}
		}
		//if the system is shutting down, don't build
		if (systemBundle.getState() == Bundle.STOPPING)
			return Status.OK_STATUS;
		try {
			doBuild(monitor);
			lastBuild = System.currentTimeMillis();
			//if the build was successful then it should not be recorded as interrupted
			setInterrupted(false);
			return Status.OK_STATUS;
		} catch (OperationCanceledException e) {
			return canceled();
		} catch (CoreException sig) {
			return sig.getStatus();
		}
	}

	/**
	 * Sets or clears the interrupted flag.
	 */
	private synchronized void setInterrupted(boolean value) {
		interrupted = value;
	}

	/**
	 * Returns true if a build is actually needed, and false otherwise.
	 */
	private synchronized boolean shouldBuild() {
		try {
			//if auto-build is off then we never run
			if (!workspace.isAutoBuilding())
				return false;
			//build if the workspace requires a build (description changes)
			if (forceBuild)
				return true;
			if (avoidBuild)
				return false;
			//return whether there have been any changes to the workspace tree.
			return buildNeeded;
		} finally {
			//regardless of the result, clear the build flags for next time
			forceBuild = avoidBuild = buildNeeded = false;
		}
	}
}
