/* -*- tab-width: 4 -*-
 *
 * Electric(tm) VLSI Design System
 *
 * File: Job.java
 *
 * Copyright (c) 2003, Oracle and/or its affiliates. All rights reserved.
 *
 * Electric(tm) is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Electric(tm) is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.sun.electric.tool;

import com.sun.electric.database.EditingPreferences;
import com.sun.electric.database.Environment;
import com.sun.electric.database.Snapshot;
import com.sun.electric.database.hierarchy.Cell;
import com.sun.electric.database.hierarchy.EDatabase;
import com.sun.electric.database.hierarchy.Library;
import com.sun.electric.database.id.CellId;
import com.sun.electric.database.id.IdManager;
import com.sun.electric.database.id.IdReader;
import com.sun.electric.database.id.IdWriter;
import com.sun.electric.database.id.LibId;
import com.sun.electric.database.id.TechId;
import com.sun.electric.database.text.Pref;
import com.sun.electric.database.variable.UserInterface;
import com.sun.electric.technology.TechPool;
import com.sun.electric.technology.Technology;
import com.sun.electric.tool.user.ActivityLogger;
import com.sun.electric.tool.user.CantEditException;
import com.sun.electric.tool.user.ErrorLogger;
import com.sun.electric.tool.util.concurrent.debug.Debug;
import com.sun.electric.util.ElapseTimer;

import java.io.IOException;
import java.io.Serializable;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import javax.swing.SwingUtilities;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Jobs are processes that will run in the background, such as DRC, NCC,
 * Netlisters, etc. Each Job gets placed in a Job window, and reports its
 * status. A job can be canceled.
 * 
 * <p>
 * To start a new job, do:
 * <p>
 * Job job = new Job(name);
 * <p>
 * job.start();
 * 
 * <p>
 * Job subclass must implement "doIt" method, it may override "terminateOK"
 * method.
 * <p>
 * Job subclass is activated by "Job.startJob()". In case of exception in
 * constructor it is not activated. Hence "doIt" and "terminateOK" method are
 * not called.
 * <p>
 * Job subclass must be serializable for CHANGE and REMOTE_EXAMINE mode.
 * Serialization occurs at the moment when "Job.startJob()" is called. Fields
 * that are not needed on the server are escaped from serialization by
 * "transient" keyword. "doIt" is executed on serverDatabase for CHANGE and
 * REMOTE_EXAMINE mode. "doIt" is executed on clientDatabase for EXAMINE mode,
 * Job subclass need not be serializable.
 * <p>
 * "doIt" may return true, may return false, may throw JobException or any other
 * Exception/Error. Return true is considered normal termination. Return false
 * and throwing any Exception/Throwable are failure terminations.
 * <p>
 * On normal termination in CHANGE or REMOTE_EXAMINE mode "fieldVariableChanged"
 * variables are serialized. In case of REMOTE_EXAMINE they are serialized on
 * read-only database state. In case of CHANGE they are serialized on database
 * state after Constraint propagation. In case of EXAMINE they are not
 * serialized, but they are checked for valid field names. Some time later the
 * changed variables are deserialized on client database. If serialization on
 * server and deserialization on client was OK then terminateOK method is called
 * on client database for all three modes CHANGE, REMOTE_EXAMINE, EXAMINE. If
 * serialization/deserialization failed then terminateOK is not called, error
 * message is issued.
 * <p>
 * In case of failure termination no terminateOK is called, error message is
 * issued,
 * 
 * <p>
 * The extending class may override getProgress(), which returns a string
 * indicating the current status. Job also contains boolean abort, which gets
 * set when the user decides to abort the Job. The extending class' code should
 * check abort when/where applicable.
 * 
 * <p>
 * Note that if your Job calls methods outside of this thread that access shared
 * data, those called methods should be synchronized.
 * 
 * @author gainsley
 */

public abstract class Job implements Serializable {

	private static boolean GLOBALDEBUG = false;
	static final int PROTOCOL_VERSION = 20; // Mar 17
	public static boolean LOCALDEBUGFLAG; // Gilda's case
	// private static final String CLASS_NAME = Job.class.getName();
	static final Logger logger = LoggerFactory.getLogger(Job.class);

	/**
	 * Method to tell whether Electric is running in "debug" mode. If the
	 * program is started with the "-debug" switch, debug mode is enabled.
	 * 
	 * @return true if running in debug mode.
	 */
	public static boolean getDebug() {
		return GLOBALDEBUG;
	}

	public static void setDebug(boolean f) {
		GLOBALDEBUG = f;
		Debug.setDebug(GLOBALDEBUG);
	}

	// ---------------------------- public methods ---------------------------

	/**
	 * Type is a typesafe enum class that describes the type of job (CHANGE or
	 * EXAMINE).
	 */
	public static enum Type {
		/** Describes a server database change. */
		CHANGE,
		/** Describes a server database undo/redo. */
		UNDO,
		/** Describes a server database examination. */
		SERVER_EXAMINE,

		/** Describes a client database examination. */
		CLIENT_EXAMINE;

		public boolean isExamine() {
			return this == Job.Type.CLIENT_EXAMINE || this == Job.Type.SERVER_EXAMINE;
		}
	}

	/**
	 * Priority is a typesafe enum class that describes the priority of a job.
	 */
	public static enum Priority {
		/** The highest priority: from the user. */
		USER,
		/** Next lower priority: visible changes. */
		VISCHANGES,
		/** Next lower priority: invisible changes. */
		INVISCHANGES,
		/** Lowest priority: analysis. */
		ANALYSIS;
	}

	/** default execution time in milliseconds */
	/* private */public static final int MIN_NUM_SECONDS = 60000;
	/** job manager */
	/* private */static ServerJobManager serverJobManager;
	/** job manager */
	/* private */static ClientJobManager clientJobManager;
	static AbstractUserInterface currentUI;
	static Thread clientThread;

	/** delete when done if true */
	/* private */boolean deleteWhenDone;

	// Job Status
	/** job start time */
	public/* protected */ElapseTimer timer = ElapseTimer.createInstance();
	/** was job started? */
	/* private */boolean started;
	/** is job finished? */
	/* private */public boolean finished;
	/** thread aborted? */
	/* private */boolean aborted;
	/** schedule thread to abort */
	/* private */boolean scheduledToAbort;
	/** report execution time regardless MIN_NUM_SECONDS */
	/* private */public boolean reportExecution = false;
	/** tool running the job */
	/* private */Tool tool;
	/** current technology */
	final TechId curTechId;
	/** current library */
	final LibId curLibId;
	/** current Cell */
	final CellId curCellId;
	// /** priority of job */ private Priority priority;
	// /** bottom of "up-tree" of cells affected */private Cell upCell;
	// /** top of "down-tree" of cells affected */ private Cell downCell;
	// /** status */ private String status = null;

	transient EJob ejob;
	transient EDatabase database;

	public static void initJobManager(int numThreads, String loggingFilePath, int socketPort, AbstractUserInterface ui,
			Job initDatabaseJob) {
		currentUI = ui;
		serverJobManager = new ServerJobManager(numThreads, loggingFilePath, false, socketPort);
		serverJobManager.runLoop(initDatabaseJob);
	}

	public static void pipeServer(int numThreads, String loggingFilePath, int socketPort) {
		Pref.forbidPreferences();
		EDatabase.setServerDatabase(new EDatabase(IdManager.stdIdManager.getInitialSnapshot()));
		Tool.initAllTools();
		serverJobManager = new ServerJobManager(numThreads, loggingFilePath, true, socketPort);
	}

	public static void socketClient(String serverMachineName, int socketPort, AbstractUserInterface ui,
			Job initDatabaseJob) {
		currentUI = ui;
		try {
			clientJobManager = new ClientJobManager(serverMachineName, socketPort);
			clientJobManager.runLoop(initDatabaseJob);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	public static void pipeClient(Process process, AbstractUserInterface ui, Job initDatabaseJob, boolean skipOneLine) {
		currentUI = ui;
		try {
			clientJobManager = new ClientJobManager(process, skipOneLine);
			clientJobManager.runLoop(initDatabaseJob);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	// public static Mode getRunMode() { return threadMode; }

	// public static int getNumThreads() { return recommendedNumThreads; }

	/**
	 * Constructor creates a new instance of Job.
	 * 
	 * @param jobName
	 *            a string that describes this Job.
	 * @param tool
	 *            the Tool that originated this Job.
	 * @param jobType
	 *            the Type of this Job (EXAMINE or CHANGE).
	 * @param upCell
	 *            the Cell at the bottom of a hierarchical "up cone" of change.
	 *            If this and "downCell" are null, the entire database is
	 *            presumed.
	 * @param downCell
	 *            the Cell at the top of a hierarchical "down tree" of
	 *            changes/examinations. If this and "upCell" are null, the
	 *            entire database is presumed.
	 * @param priority
	 *            the priority of this Job.
	 */
	public Job(String jobName, Tool tool, Type jobType, Cell upCell, Cell downCell, Priority priority) {
		UserInterface ui = getUserInterface();
		ejob = new EJob(this, jobType, jobName, EditingPreferences.getInstance());
		database = ui != null ? ui.getDatabase() : EDatabase.clientDatabase();
		this.tool = tool;
		this.deleteWhenDone = true;
		Technology curTech = ui != null ? ui.getCurrentTechnology() : null;
		curTechId = curTech != null ? curTech.getId() : null;
		Library curLib = ui != null ? ui.getCurrentLibrary() : null;
		curLibId = curLib != null ? curLib.getId() : null;
		Cell curCell = ui != null ? ui.getCurrentCell() : null;
		curCellId = curCell != null ? curCell.getId() : null;
		// started = finished = aborted = scheduledToAbort = false;
		// thread = null;
	}

	/**
	 * Start a job. By default displays Job on Job List UI, and delete Job when
	 * done. Jobs that have state the user would like to use after the Job
	 * finishes (like DRC, NCC, etc) should call
	 * <code>startJob(true, true)</code>.
	 */
	public void startJob() {
		startJob(true);
	}

	/**
	 * Start a job on snapshot obtained at the end of current job. By default
	 * displays Job on Job List UI, and delete Job when done.
	 */
	public void startJobOnMyResult() {
		startJob(true, true);
	}

	/**
	 * Start the job by placing it on the JobThread queue. If
	 * <code>deleteWhenDone</code> is true, Job will be deleted after it is done
	 * (frees all data and references it stores/created)
	 * 
	 * @param deleteWhenDone
	 *            delete when job is done if true, otherwise leave it around
	 */
	public void startJob(boolean deleteWhenDone) {
		startJob(deleteWhenDone, false);
	}

	/**
	 * Start the job by placing it on the JobThread queue. If
	 * <code>deleteWhenDone</code> is true, Job will be deleted after it is done
	 * (frees all data and references it stores/created)
	 * 
	 * @param deleteWhenDone
	 *            delete when job is done if true, otherwise leave it around
	 */
	private void startJob(boolean deleteWhenDone, boolean onMySnapshot) {
		this.deleteWhenDone = deleteWhenDone;

		UserInterface ui = getUserInterface();
		Job.Key curJobKey = ui.getJobKey();
		boolean startedByServer = curJobKey.doItOnServer;
		boolean doItOnServer = ejob.jobType != Job.Type.CLIENT_EXAMINE;
		if (startedByServer) {
			assert doItOnServer;
			ejob.client = Job.serverJobManager.serverConnections.get(curJobKey.clientId);
			ejob.jobKey = ejob.client.newJobId(startedByServer, doItOnServer);
			timer.start();
			ejob.serialize(EDatabase.serverDatabase());
			ejob.clientJob = null;
			Job.serverJobManager.addJob(ejob, onMySnapshot);
		} else {
			ejob.client = Job.currentUI;
			ejob.jobKey = ejob.client.newJobId(startedByServer, doItOnServer);
			timer.start();
			ejob.serverJob = null;
			if (doItOnServer) {
				ejob.serialize(EDatabase.clientDatabase());
				Job.currentUI.putProcessingEJob(ejob, onMySnapshot);
				if (serverJobManager != null) {
					serverJobManager.addJob(ejob, onMySnapshot);
				} else {
					assert SwingUtilities.isEventDispatchThread();
					try {
						clientJobManager.writeEJob(ejob);
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
			} else {
				Job.currentUI.putProcessingEJob(ejob, onMySnapshot);
			}
		}
	}
    
    protected void showSnapshot() {
        assert ejob.jobType == Type.CHANGE;
        EThread thread = (EThread)Thread.currentThread();
        assert thread.isServerThread;
        assert thread.ejob == ejob;
        Snapshot snapshot = EDatabase.serverDatabase().backup();
        Client.fireServerEvent(new Client.SnapshotEvent(snapshot));
    }

	/**
	 * Method to remember that a field variable of the Job has been changed by
	 * the doIt() method.
	 * 
	 * @param variableName
	 *            the name of the variable that changed.
	 */
	protected void fieldVariableChanged(String variableName) {
		ejob.fieldVariableChanged(variableName);
	}

	// --------------------------ABSTRACT METHODS--------------------------

	/**
	 * This is the main work method. This method should perform all needed
	 * tasks.
	 * 
	 * @throws JobException
	 *             TODO
	 */
	public abstract boolean doIt() throws JobException;

	/**
	 * This method executes in the Client side after termination of doIt method.
	 * This method should perform all needed termination actions.
	 * 
	 * @param jobException
	 *            null if doIt terminated normally, otherwise exception thrown
	 *            by doIt.
	 */
	public void terminateIt(Throwable jobException) {
		if (jobException == null)
			terminateOK();
		else
			terminateFail(jobException);
	}

	/**
	 * This method executes in the Client side after normal termination of doIt
	 * method. This method should perform all needed termination actions.
	 */
	public void terminateOK() {
	}

	/**
	 * This method executes in the Client side after exceptional termination of
	 * doIt method.
	 * 
	 * @param jobException
	 *            null exception thrown by doIt.
	 */
	public void terminateFail(Throwable jobException) {
		if (jobException instanceof CantEditException) {
			((CantEditException) jobException).presentProblem();
		} else if (jobException instanceof JobException) {
			String message = jobException.getMessage();
			if (message == null)
				message = "Job " + ejob.jobName + " failed";
			System.out.println(message);
		} else {
			ActivityLogger.logException(jobException);
		}
	}

	// --------------------------PRIVATE JOB METHODS--------------------------

	// --------------------------PROTECTED JOB METHODS-----------------------
	/** Set reportExecution flag on/off */
	protected void setReportExecutionFlag(boolean flag) {
		reportExecution = flag;
	}

	// --------------------------PUBLIC JOB METHODS--------------------------

	// /**
	// * Method to end the current batch of changes and start another.
	// * Besides starting a new batch, it cleans up the constraint system, and
	// */
	// protected void flushBatch()
	// {
	// if (jobType != Type.CHANGE) return;
	// Undo.endChanges();
	// Undo.startChanges(tool, jobName, /*upCell,*/ savedHighlights,
	// savedHighlightsOffset);
	// }

	protected synchronized void setProgress(String progress) {
		if (serverJobManager != null)
			serverJobManager.setProgress(ejob, progress);
		else
			ejob.progress = progress;
	}

	private synchronized String getProgress() {
		return ejob.progress;
	}

	/** Return run status */
	public boolean isFinished() {
		return finished;
	}

	/**
	 * Tell thread to abort. Extending class should check abort when/where
	 * applicable
	 */
	public synchronized void abort() {
		// if (ejob.jobType != Job.Type.EXAMINE) return;
		if (aborted) {
			System.out.println("Job already aborted: " + getStatus());
			return;
		}
		scheduledToAbort = true;
		if (ejob.jobType != Job.Type.CLIENT_EXAMINE && ejob.serverJob != null)
			ejob.serverJob.scheduledToAbort = true;
		// Job.getUserInterface().wantToRedoJobTree();
	}

	/** Confirmation that thread is aborted */
	private synchronized void setAborted() {
		aborted = true; /* Job.getUserInterface().wantToRedoJobTree(); */
	}

	/** get scheduled to abort status */
	protected synchronized boolean getScheduledToAbort() {
		return scheduledToAbort;
	}

	/**
	 * Method to get abort status. Please leave this function as private. To
	 * retrieve abort status from another class, use checkAbort which also
	 * checks if job is scheduled to be aborted.
	 * 
	 * @return
	 */
	private synchronized boolean getAborted() {
		return aborted;
	}

	/** get deleteWhenDone status */
	public boolean getDeleteWhenDone() {
		return deleteWhenDone;
	}

	/**
	 * Check if we are scheduled to abort. If so, print message if non null and
	 * return true. This is because setAbort and getScheduledToAbort are
	 * protected in Job.
	 * 
	 * @return true on abort, false otherwise. If job is scheduled for abort or
	 *         aborted. and it will report it to standard output
	 */
	public boolean checkAbort() {
		// if (ejob.jobType != Job.Type.EXAMINE) return false;
		if (getAborted())
			return (true);
		boolean scheduledAbort = getScheduledToAbort();
		if (scheduledAbort) {
			System.out.println(this + ": aborted"); // should call
			// Job.toString()
			setReportExecutionFlag(true); // Force reporting
			setAborted(); // Job has been aborted
		}
		return scheduledAbort;
	}

	/** get all jobs iterator */
	public static Iterator<Job> getAllJobs() {
		return serverJobManager != null ? serverJobManager.getAllJobs() : currentUI.getAllJobs().iterator();
	}

	/**
	 * If this current thread is a EThread running a Job return the Job. Return
	 * null otherwise.
	 * 
	 * @return a running Job or null
	 */
	public static Job getRunningJob() {
		Thread thread = Thread.currentThread();
		return thread instanceof EThread ? ((EThread) thread).getRunningJob() : null;
	}

	/**
	 * Returns true if this current thread is a EThread running a server Job.
	 * 
	 * @return true if this current thread is a EThread running a server Job.
	 */
	public static boolean inServerThread() {
		Thread thread = Thread.currentThread();
		return thread instanceof EThread && ((EThread) thread).isServerThread;
	}

	public static void setCurrentLibraryInJob(Library lib) {
		EThread thread = (EThread) Thread.currentThread();
		assert thread.ejob.jobType == Type.CHANGE;
		thread.userInterface.curLibId = lib != null ? lib.getId() : null;
	}

	/** get status */
	public String getStatus() {
		switch (ejob.state) {
		// case CLIENT_WAITING: return "cwaiting";
		case WAITING:
			return "waiting";
		case RUNNING:
			return getProgress() == null ? "running" : getProgress();
		case SERVER_DONE:
			return getProgress() == null ? "done" : getProgress();
		case CLIENT_DONE:
			return "cdone";
		}
		if (!started)
			return "waiting";
		if (aborted)
			return "aborted";
		if (finished)
			return "done";
		if (scheduledToAbort)
			return "scheduled to abort";
		if (getProgress() == null)
			return "running";
		return getProgress();
	}

	/** Remove job from Job list if it is done */
	public boolean remove() {
		if (!finished && !aborted) {
			// System.out.println("Cannot delete running jobs.  Wait till finished or abort");
			return false;
		}
		if (serverJobManager != null)
			serverJobManager.removeJob(this);
		else
			currentUI.removeProcessingEJob(getKey());
		return true;
	}

	private static final ThreadLocal<UserInterface> threadUserInterface = new ThreadLocal<UserInterface>() {
		// @Override
		// protected UserInterface initialValue() {
		// throw new IllegalStateException();
		// }
	};

	public static UserInterface getUserInterface() {
		Thread currentThread = Thread.currentThread();
		if (currentThread instanceof EThread)
			return ((EThread) currentThread).getUserInterface();
		else if (currentThread == clientThread)
			return currentUI;
		else
			return threadUserInterface.get();
	}

	public static void setUserInterface(UserInterface ui) {
		Thread currentThread = Thread.currentThread();
		assert !(currentThread instanceof EThread) && currentThread != clientThread;
		if (ui == null)
			throw new UnsupportedOperationException();
		threadUserInterface.set(ui);
	}

	/**
	 * Low-level method.
	 */
	public static AbstractUserInterface getExtendedUserInterface() {
		// if (!isClientThread())
		// ActivityLogger.logException(new IllegalStateException());
		return currentUI;
	}

	public static boolean isClientThread() {
		return Thread.currentThread() == clientThread;
	}

	public EDatabase getDatabase() {
		return database;
	}

	public Environment getEnvironment() {
		return database.getEnvironment();
	}

	public TechPool getTechPool() {
		return database.getTechPool();
	}

	public Tool getTool() {
		return tool;
	}

	public EditingPreferences getEditingPreferences() {
		return ejob.editingPreferences;
	}

	public static void updateNetworkErrors(Cell cell, List<ErrorLogger.MessageLog> errors) {
		if (currentUI != null)
			currentUI.updateNetworkErrors(cell, errors);
	}

	public static void updateIncrementalDRCErrors(Cell cell, List<ErrorLogger.MessageLog> newErrors,
			List<ErrorLogger.MessageLog> delErrors) {
		currentUI.updateIncrementalDRCErrors(cell, newErrors, delErrors);
	}

	/**
	 * Find some valid snapshot in cache.
	 * 
	 * @return some valid snapshot
	 */
	public static Snapshot findValidSnapshot() {
		return EThread.findValidSnapshot();
	}

	// -------------------------------JOB UI--------------------------------

	public String toString() {
		return ejob.jobName + " (" + getStatus() + ")";
	}

	/**
	 * Print a message, dump a stack trace, and throw a RuntimeException if
	 * errorHasOccurred argument is true.
	 * 
	 * @param errorHasOccurred
	 *            indicates a runtime error has been detected
	 * @param msg
	 *            the message to print when an error occurs
	 * @throws RuntimeException
	 *             if errorHasOccurred is true
	 */
	public static void error(boolean errorHasOccurred, String msg) {
		if (!errorHasOccurred)
			return;
		RuntimeException e = new RuntimeException(msg);
		// The following prints a stack trace on the console
		ActivityLogger.logException(e);

		// The following prints a stack trace in the Electric messages window
		throw e;
	}

	/** Get info on Job */
	public String getInfo() {
		StringBuffer buf = new StringBuffer();
		buf.append("Job " + toString());
		// buf.append("  start time: "+start+"\n");
		if (finished) {
			// buf.append("  end time: "+end+"\n");
			buf.append(" took: " + timer);
			Date start = new Date(timer.getStart());
			Date end = new Date(timer.getEnd());
			buf.append(" (started at " + start + ", ended at " + end + " )");
			
		} else if (getProgress() == null) {
			buf.append(" has not finished. Current running time: " + timer);
		} else {
			buf.append(" did not successfully finish.");
		}
		return buf.toString();
	}

	// static void runTerminate(EJob ejob) {
	// Throwable jobException = ejob.deserializeResult();
	// Job job = ejob.clientJob;
	// try {
	// job.terminateIt(jobException);
	// } catch (Throwable e) {
	// System.out.println("Exception executing terminateIt");
	// e.printStackTrace(System.out);
	// }
	// job.endTime = System.currentTimeMillis();
	// job.finished = true; // is this redundant with Thread.isAlive()?
	//
	// // say something if it took more than a minute by default
	// if (job.reportExecution || (job.endTime - job.startTime) >=
	// MIN_NUM_SECONDS) {
	// if (User.isBeepAfterLongJobs()) {
	// Toolkit.getDefaultToolkit().beep();
	// }
	// System.out.println(job.getInfo());
	// }
	// }

	public Key getKey() {
		return ejob.jobKey;
	}

	public Inform getInform() {
		return new Inform(this);
	}

	/**
	 * Method to tell whether this Job is an examine or change job.
	 * @return true for Examine, false for Change.
	 */
	public boolean isExamine() {
		return ejob.isExamine();
	}

	/**
	 * Identifies a Job in a given Electric client/server session. Job obtains
	 * its Key in startJob method. Also can identify Jobless context (for
	 * example Client's Gui)
	 */
	public static class Key implements Serializable {
		/**
		 * Client which launched the Job
		 */
		public final int clientId;
		/**
		 * Job id. 0 - Jobless context positive - Job started from server side
		 * negative - Job started from client side
		 */
		public final int jobId;
		public final boolean doItOnServer;

		Key(int clientId, int jobId, boolean doItOnServer) {
			this.clientId = clientId;
			this.jobId = jobId;
			this.doItOnServer = doItOnServer;
		}

		Key(Client client, int jobId, boolean doItOnServer) {
			this(client.connectionId, jobId, doItOnServer);
		}

		public boolean startedByServer() {
			return jobId > 0;
		}

		@Override
		public boolean equals(Object o) {
			if (o == this)
				return true;
			if (o instanceof Key) {
				Key that = (Key) o;
				if (this.clientId == that.clientId && this.jobId == that.jobId) {
					assert this.doItOnServer == that.doItOnServer;
					return true;
				}
			}
			return false;
		}

		@Override
		public int hashCode() {
			return jobId;
		}

		public void write(IdWriter writer) throws IOException {
			writer.writeInt(clientId);
			writer.writeInt(jobId);
			writer.writeBoolean(doItOnServer);
		}

		public static Key read(IdReader reader) throws IOException {
			int clientId = reader.readInt();
			int jobId = reader.readInt();
			boolean doItOnServer = reader.readBoolean();
			return new Key(clientId, jobId, doItOnServer);
		}
	}

	public static class Inform implements Serializable {
		private final Key jobKey;
		private final boolean isChange;
		private final String toString;
		private final int finished;
		private ElapseTimer timer;

		Inform(Job job) {
			jobKey = job.getKey();
			isChange = (job.ejob.jobType == Type.CHANGE) || (job.ejob.jobType == Type.UNDO);
			toString = job.toString();
			timer = job.timer;
			if (job.finished)
				finished = 1;
			else if (job.getProgress() == null)
				finished = 0;
			else
				finished = -1;
		}

		Inform(Key jobKey, boolean isChange, String toString, ElapseTimer timer, int finished) {
			this.jobKey = jobKey;
			this.isChange = isChange;
			this.toString = toString;
			this.timer = timer;
			this.finished = finished;
		}

		public void abort() {
			for (Iterator<Job> it = getAllJobs(); it.hasNext();) {
				Job job = it.next();
				if (job.getKey().equals(jobKey)) {
					job.abort();
					break;
				}
			}
		}

		public boolean remove() {
			return false; /* return job.remove(); */
		}

		public Key getKey() {
			return jobKey;
		}

		@Override
		public String toString() {
			return toString;
		}

		public String getInfo() {
			StringBuilder buf = new StringBuilder();
			buf.append("Job " + toString());
			// buf.append("  start time: "+start+"\n");
			if (finished == 1) {
				// Date end = new Date(endTime);
				// buf.append("  end time: "+end+"\n");
				buf.append(" took: " + timer);
				Date start = new Date(timer.getStart());
				buf.append(" (started at " + start + ")");
			} else if (finished == 0) {
				buf.append(" has not finished. Current running time: " + timer.currentTimeString());
			} else {
				buf.append(" did not successfully finish.");
			}
			return buf.toString();
		}

		public boolean isChangeJobQueuedOrRunning() {
			return finished != 1 && isChange;
		}

		public void write(IdWriter writer) throws IOException {
			jobKey.write(writer);
			writer.writeBoolean(isChange);
			writer.writeString(toString);
			writer.writeLong(timer.getStart());
			writer.writeLong(timer.getEnd());
			writer.writeInt(finished);
		}

		public static Inform read(IdReader reader) throws IOException {
			Key jobKey = Key.read(reader);
			boolean isChange = reader.readBoolean();
			String toString = reader.readString();
			long startTime = reader.readLong();
			long endTime = reader.readLong();
			ElapseTimer timer = ElapseTimer.createInstanceByValues(startTime, endTime);
			int finished = reader.readInt();
			return new Inform(jobKey, isChange, toString, timer, finished);
		}
	}
}
