/*******************************************************************************
 * Copyright (c) 2001, 2018 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Jens Lukowski/Innoopract - initial renaming/restructuring
 *     
 *******************************************************************************/
package org.eclipse.wst.sse.ui.internal.reconcile;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.DocumentRewriteSessionEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentExtension3;
import org.eclipse.jface.text.IDocumentExtension4;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.IDocumentRewriteSessionListener;
import org.eclipse.jface.text.ITextInputListener;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.text.reconciler.DirtyRegion;
import org.eclipse.jface.text.reconciler.IReconciler;
import org.eclipse.jface.text.reconciler.IReconcilerExtension;
import org.eclipse.jface.text.reconciler.IReconcilingStrategy;
import org.eclipse.jface.text.reconciler.IReconcilingStrategyExtension;
import org.eclipse.wst.sse.ui.internal.Logger;

/**
 * This Job holds a queue of updates from the editor (DirtyRegions) to
 * process. When a new request comes in, the current run is canceled, the new
 * request is added to the queue, then the job is re-scheduled.
 * 
 * @author pavery
 */
public class DirtyRegionProcessor implements IReconciler, IReconcilerExtension, IConfigurableReconciler {
	class DocumentListener implements IDocumentListener {
		public void documentAboutToBeChanged(DocumentEvent event) {
			/*
			 * if in rewrite session and already going to reprocess entire
			 * document after rewrite session, do nothing
			 */
			if (isInRewriteSession() && fReprocessAfterRewrite)
				return;
			// save partition type (to see if it changes in documentChanged())
			fLastPartitions = getPartitionRegions(event.getOffset(), event.getLength());
		}

		public void documentChanged(DocumentEvent event) {
			/*
			 * if in rewrite session and already going to reprocess entire
			 * document after rewrite session, do nothing
			 */
			if (isInRewriteSession() && fReprocessAfterRewrite)
				return;

			if (partitionsChanged(event)) {
				// pa_TODO
				// this is a simple way to ensure old
				// annotations are removed when partition changes

				// it might be a performance hit though
				setEntireDocumentDirty(getDocument());
			}
			else {
				/*
				 * bug 384776 - no wrong text may be included 
				 */
				DirtyRegion dr = null;
				if (event.getLength() == 0) {
					/*
					 * It's an insert-- we use text length though so that the
					 * new region gets validated...
					 */
					dr = createDirtyRegion(event.getOffset(), event.getText().length(), DirtyRegion.INSERT);
				}
				else {
					if ("".equals(event.getText())) { //$NON-NLS-1$
						// it's a delete-- we use text length=0
						dr = createDirtyRegion(event.getOffset(), 0, DirtyRegion.REMOVE);
					}
					else {
						// it's a replace-- we use text length though so that the new region gets validated...
						dr = createDirtyRegion(event.getOffset(), event.getText().length(), DirtyRegion.INSERT);
					}
				}
				if (isInRewriteSession()) {
					/*
					 * while in rewrite session, found a dirty region, so flag
					 * that entire document needs to be reprocesed after
					 * rewrite session
					 */
					if (!fReprocessAfterRewrite && (dr != null)) {
						fReprocessAfterRewrite = true;
					}
				}
				else {
					processDirtyRegion(dr);
				}
			}
		}

		/**
		 * Checks previous partitions from the span of the event w/ the new
		 * partitions from the span of the event. If partitions changed,
		 * return true, else return false
		 * 
		 * @param event
		 * @return
		 */
		private boolean partitionsChanged(DocumentEvent event) {
			boolean changed = false;
			int length = event.getText().length(); // In any case we want partitions of the new text (even if its length is zero)

			ITypedRegion[] newPartitions = getPartitionRegions(event.getOffset(), length);
			if (fLastPartitions != null) {
				if (fLastPartitions.length != newPartitions.length) {
					changed = true;
				}
				else {
					for (int i = 0; i < fLastPartitions.length; i++) {
						if (!fLastPartitions[i].getType().equals(newPartitions[i].getType())) {
							changed = true;
							break;
						}
					}
				}
			}
			return changed;
		}
	}

	/**
	 * Reconciles the entire document when the document in the viewer is
	 * changed. This happens when the document is initially opened, as well as
	 * after a save-as.
	 * 
	 * Also see processPostModelEvent(...) for similar behavior when document
	 * for the model is changed.
	 */
	class TextInputListener implements ITextInputListener {
		public void inputDocumentAboutToBeChanged(IDocument oldInput, IDocument newInput) {
			// do nothing
		}

		public void inputDocumentChanged(IDocument oldInput, IDocument newInput) {
			handleInputDocumentChanged(oldInput, newInput);

			startReconciling();
		}
	}

	class DocumentRewriteSessionListener implements IDocumentRewriteSessionListener {
		long time0 = 0;

		public void documentRewriteSessionChanged(DocumentRewriteSessionEvent event) {
			boolean oldValue = fInRewriteSession;
			fInRewriteSession = event != null && event.getChangeType().equals(DocumentRewriteSessionEvent.SESSION_START);

			if (event.getChangeType().equals(DocumentRewriteSessionEvent.SESSION_START)) {
				if (DEBUG) {
					time0 = System.currentTimeMillis();
				}
				// bug 235446 - source validation annotations lost after rewrite session
				if (!getDirtyRegionQueue().isEmpty()) {
					flushDirtyRegionQueue();
					fReprocessAfterRewrite = true;	
				} else {
					fReprocessAfterRewrite = false;
				}
			}
			else if (event.getChangeType().equals(DocumentRewriteSessionEvent.SESSION_STOP)) {
				if (fInRewriteSession ^ oldValue && fDocument != null) {
					if (DEBUG) {
						Logger.log(Logger.INFO, "Rewrite session lasted " + (System.currentTimeMillis() - time0) + "ms");
						time0 = System.currentTimeMillis();
					}
					if (fReprocessAfterRewrite) {
						DirtyRegion entireDocument = createDirtyRegion(0, fDocument.getLength(), DirtyRegion.INSERT);
						processDirtyRegion(entireDocument);
					}
					if (DEBUG) {
						Logger.log(Logger.INFO, "Full document reprocess took " + (System.currentTimeMillis() - time0) + "ms");
					}
					fReprocessAfterRewrite = false;
				}
			}
		}
	}

	/** debug flag */
	protected static final boolean DEBUG;
	private static final long UPDATE_DELAY = 500;

	static {
		String value = Platform.getDebugOption("org.eclipse.wst.sse.ui/debug/reconcilerjob"); //$NON-NLS-1$
		DEBUG = value != null && value.equalsIgnoreCase("true"); //$NON-NLS-1$
	}

	private long fDelay;


	/** local queue of dirty regions (created here) to be reconciled */
	private List fDirtyRegionQueue = Collections.synchronizedList(new ArrayList());

	/** document that this reconciler works on */
	private IDocument fDocument = null;

	private IDocumentListener fDocumentListener = new DocumentListener();
	private IDocumentRewriteSessionListener fDocumentRewriteSessionListener = new DocumentRewriteSessionListener();

	/**
	 * set true after first install to prevent duplicate work done in the
	 * install method (since install gets called multiple times)
	 */
	private boolean fIsInstalled = false;

	/**
	 * so we can tell if a partition changed after the last edit
	 */
	ITypedRegion[] fLastPartitions;

	List fNonIncrementalStrategiesAlreadyProcessed = new ArrayList(1);

	/**
	 * The partitioning this reconciler uses.
	 */
	private String fPartitioning;

	Map<String, IReconcilingStrategy> fReconcilingStrategies = null;

	/** for initial reconcile when document is opened */
	private TextInputListener fTextInputListener = null;
	/** the text viewer */
	private ITextViewer fViewer;
	boolean fInRewriteSession = false;
	/**
	 * true if entire document needs to be reprocessed after rewrite session
	 */
	boolean fReprocessAfterRewrite = false;

	/** The job should be reset because of document changes */
	private boolean fReset = false;
	private boolean fIsCanceled = false;
	private boolean fHasReconciled = false;
	private Object LOCK = new Object();

	private BackgroundThread fThread;

	/**
	 * Creates a new StructuredRegionProcessor
	 */
	public DirtyRegionProcessor() {
		// init reconciler stuff
		setDelay(UPDATE_DELAY);
		fReconcilingStrategies = new HashMap<String, IReconcilingStrategy>();
	}

	/**
	 * Adds the given resource to the set of resources that need refreshing.
	 * Synchronized in order to protect the collection during add.
	 * 
	 * @param resource
	 */
	private synchronized void addRequest(DirtyRegion newDirtyRegion) {
		// NOTE: This method is called a lot so make sure it's fast
		List dirtyRegionQueue = getDirtyRegionQueue();
		synchronized (dirtyRegionQueue) {
			for (Iterator it = dirtyRegionQueue.iterator(); it.hasNext();) {
				// go through list of existing dirty regions and check if any
				// dirty regions need to be discarded
				DirtyRegion currentExisting = (DirtyRegion) it.next();
				DirtyRegion outer = getOuterRegion(currentExisting, newDirtyRegion);
				// if we already have a request which contains the new request,
				// discard the new request
				if (outer == currentExisting)
					return;
				// if new request contains any existing requests,
				// remove those
				if (outer == newDirtyRegion)
					it.remove();
			}
			dirtyRegionQueue.add(newDirtyRegion);
		}
	}

	/**
	 * Notifies subclasses that processing of multiple dirty regions has begun
	 */
	protected void beginProcessing() {
		// do nothing by default
	}

	/**
	 * @param dirtyRegion
	 * @return
	 */
	protected ITypedRegion[] computePartitioning(DirtyRegion dirtyRegion) {
		int drOffset = dirtyRegion.getOffset();
		int drLength = dirtyRegion.getLength();

		return computePartitioning(drOffset, drLength);
	}

	protected ITypedRegion[] computePartitioning(int drOffset, int drLength) {
		ITypedRegion[] tr = new ITypedRegion[0];
		IDocument doc = getDocument();
		if (doc != null){
			int docLength = doc.getLength();
	
			if (drOffset > docLength) {
				drOffset = docLength;
				drLength = 0;
			}
			else if (drOffset + drLength > docLength) {
				drLength = docLength - drOffset;
			}
	
			try {
				// dirty region may span multiple partitions
				tr = TextUtilities.computePartitioning(doc, getDocumentPartitioning(), drOffset, drLength, true);
			}
			catch (BadLocationException e) {
				String info = "dr: [" + drOffset + ":" + drLength + "] doc: [" + docLength + "] "; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
				Logger.logException(info, e);
				tr = new ITypedRegion[0];
			}
		}
		return tr;
	}

	/**
	 * Used to determine if one dirty region contains the other and if so,
	 * which is the one that contains it.
	 * 
	 * @param root
	 * @param possible
	 * @return the outer dirty region if it contains the other dirty region,
	 *         null otherwise
	 */
	protected DirtyRegion getOuterRegion(DirtyRegion root, DirtyRegion possible) {
		DirtyRegion outer = null;
		if (isContained(root, possible))
			outer = root;
		else if (isContained(possible, root))
			outer = possible;
		return outer;
	}

	/**
	 * Used to determine of a "possible" dirty region can be discarded in
	 * favor of using just the "root" dirty region.
	 * 
	 * @return if the root dirty region contains possible, return true,
	 *         otherwise return false
	 */
	private boolean isContained(DirtyRegion root, DirtyRegion possible) {
		int rootStart = root.getOffset();
		int rootEnd = rootStart + root.getLength();
		int possStart = possible.getOffset();
		int possEnd = possStart + possible.getLength();
		if (rootStart <= possStart && rootEnd >= possEnd)
			return true;
		return false;
	}

	protected DirtyRegion createDirtyRegion(int offset, int length, String type) {
		DirtyRegion durty = null;
		IDocument doc = getDocument();

		if (doc != null) {
			// safety for BLE
			int docLen = doc.getLength();
			if (offset > docLen) {
				offset = docLen;
				length = 0;
			}
			else if (offset + length >= docLen)
				length = docLen - offset;
			try {
				durty = new DirtyRegion(offset, length, type, doc.get(offset, length));
			}
			catch (BadLocationException e) {
				String info = "dr: [" + offset + ":" + length + "] doc: [" + docLen + "] "; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
				Logger.logException(info, e);
			}
		}
		return durty;
	}

	protected DirtyRegion createDirtyRegion(ITypedRegion tr, String type) {
		return createDirtyRegion(tr.getOffset(), tr.getLength(), type);
	}

	protected void flushDirtyRegionQueue() {
		synchronized (fDirtyRegionQueue) {
			fDirtyRegionQueue.clear();
		}
	}

	/**
	 * Notifies subclasses that processing of multiple dirty regions has
	 * ended, for now
	 */
	protected void endProcessing() {
		// do nothing by default
	}

	/**
	 * Delay between processing of DirtyRegions.
	 * 
	 * @return
	 */
	long getDelay() {
		return fDelay;
	}

	List getDirtyRegionQueue() {
		return fDirtyRegionQueue;
	}

	/**
	 * The IDocument on which this reconciler operates
	 * 
	 * @return
	 */
	protected IDocument getDocument() {
		return fDocument;
	}

	public String getDocumentPartitioning() {
		if (fPartitioning == null)
			return IDocumentExtension3.DEFAULT_PARTITIONING;
		return fPartitioning;
	}

	protected String[] getPartitions(int drOffset, int drLength) {

		ITypedRegion[] regions = new ITypedRegion[0];
		int docLength = getDocument().getLength();

		if (drOffset > docLength) {
			drOffset = docLength;
			drLength = 0;
		}
		else if (drOffset + drLength > docLength) {
			drLength = docLength - drOffset;
		}

		try {
			regions = TextUtilities.computePartitioning(getDocument(), getDocumentPartitioning(), drOffset, drLength, true);
		}
		catch (BadLocationException e) {
			Logger.logException(e);
			regions = new ITypedRegion[0];
		}
		String[] partitions = new String[regions.length];
		for (int i = 0; i < regions.length; i++)
			partitions[i] = regions[i].getType();
		return partitions;
	}

	ITypedRegion[] getPartitionRegions(int drOffset, int drLength) {
		ITypedRegion[] regions = new ITypedRegion[0];
		int docLength = getDocument().getLength();

		if (drOffset > docLength) {
			drOffset = docLength;
			drLength = 0;
		}
		else if (drOffset + drLength > docLength) {
			drLength = docLength - drOffset;
		}

		try {
			regions = TextUtilities.computePartitioning(getDocument(), getDocumentPartitioning(), drOffset, drLength, true);
		}
		catch (BadLocationException e) {
			Logger.logException(e);
			regions = new ITypedRegion[0];
		}
		return regions;
	}
		
		
	/**
	 * Returns the reconciling strategy registered with the reconciler for the
	 * specified partition type.
	 * 
	 * @param partitionType
	 *            the partition type for which to determine the reconciling
	 *            strategy
	 * 
	 * @return the reconciling strategy registered for the given partition
	 *         type, or <code>null</code> if there is no such strategy
	 * 
	 * @see org.eclipse.jface.text.reconciler.IReconciler#getReconcilingStrategy(java.lang.String)
	 */
	public IReconcilingStrategy getReconcilingStrategy(String partitionType) {
		if (partitionType == null)
			return null;
		return fReconcilingStrategies.get(partitionType);
	}

	/**
	 * This method also synchronized because it accesses the fRequests queue
	 * 
	 * @return an array of the currently requested Nodes to refresh
	 */
	private synchronized DirtyRegion[] getRequests() {
		synchronized (fDirtyRegionQueue) {
			if (0 == fDirtyRegionQueue.size()) {
				return null;
			}
			DirtyRegion[] toRefresh = (DirtyRegion[]) fDirtyRegionQueue.toArray(new DirtyRegion[fDirtyRegionQueue.size()]);
			flushDirtyRegionQueue();
			return toRefresh;
		}
	}

	/**
	 * Returns the text viewer this reconciler is installed on.
	 * 
	 * @return the text viewer this reconciler is installed on
	 */
	protected ITextViewer getTextViewer() {
		return fViewer;
	}

	/**
	 * 
	 * @param oldInput
	 * @param newInput
	 */
	void handleInputDocumentChanged(IDocument oldInput, IDocument newInput) {
		// don't bother if reconciler not installed
		if (isInstalled()) {
			setDocument(newInput);
		}
	}

	/**
	 * @see org.eclipse.jface.text.reconciler.IReconciler#install(ITextViewer)
	 */
	public void install(ITextViewer textViewer) {
		// we might be called multiple times with the same viewe.r,
		// maybe after being uninstalled as well, so track separately
		if (!isInstalled()) {
			fViewer = textViewer;
			fTextInputListener = new TextInputListener();
			textViewer.addTextInputListener(fTextInputListener);
			setInstalled(true);
		}
		synchronized (this) {
			if (fThread == null)
				fThread = new BackgroundThread(getClass().getName());
		}
	}

	/**
	 * The viewer has been set on this Reconciler.
	 * 
	 * @return true if the viewer has been set on this Reconciler, false
	 *         otherwise.
	 */
	public boolean isInstalled() {
		return fIsInstalled;
	}

	boolean isInRewriteSession() {
		return fInRewriteSession;
	}

	/**
	 * Subclasses should implement for specific handling of dirty regions. The
	 * method is invoked for each dirty region in the Job's queue.
	 * 
	 * @param dirtyRegion
	 */
	protected void process(DirtyRegion dirtyRegion) {
		if (!isInstalled() || isInRewriteSession() || dirtyRegion == null || getDocument() == null || fIsCanceled) {
			return;
		}
		/*
		 * Break the dirty region into a sequence of partitions and find the
		 * corresponding strategy to reconcile those partitions. If a strategy
		 * implements INonIncrementalReconcilingStrategy, only call it once
		 * regardless of the number and types of partitions.
		 */
		ITypedRegion[] partitions = computePartitioning(dirtyRegion);
		for (int i = 0; i < partitions.length && !fIsCanceled; i++) {
			IReconcilingStrategy strategy = getReconcilingStrategy(partitions[i].getType());
			if (strategy != null) {
				strategy.reconcile(partitions[i]);
			}
		}
	}

	/**
	 * Invoke dirty region processing.
	 * 
	 * @param node
	 */
	public final void processDirtyRegion(DirtyRegion dr) {
		if (dr == null || !isInstalled())
			return;

		addRequest(dr);
		synchronized (LOCK) {
			fReset = true;
		}

		if (DEBUG) {
			System.out.println("added request for: [" + dr.getText() + "]"); //$NON-NLS-1$ //$NON-NLS-2$
			System.out.println("queue size is now: " + getDirtyRegionQueue().size()); //$NON-NLS-1$
		}
	}

	/**
	 * Based on {@link org.eclipse.jface.text.reconciler.AbstractReconciler.BackgroundThread}
	 *
	 */
	class BackgroundThread extends Thread {

		public BackgroundThread(String name) {
			super(name);
			setPriority(Thread.MIN_PRIORITY);
			setDaemon(true);
		}

		public void cancel() {
			synchronized (fDirtyRegionQueue) {
				fDirtyRegionQueue.notifyAll();
			}
		}

		public void run() {
			synchronized (fDirtyRegionQueue) {
				try {
					fDirtyRegionQueue.wait(getDelay());
				} catch (InterruptedException e) {}
			}

			if (fIsCanceled)
				return;

			if (!fHasReconciled) {
				initialReconcile();
				fHasReconciled = true;
			}

			while (!fIsCanceled) {
				synchronized (fDirtyRegionQueue) {
					try {
						fDirtyRegionQueue.wait(getDelay());
					} catch (InterruptedException e) {}
				}

				if (fIsCanceled)
					return;

				synchronized (LOCK) {
					if (fReset) {
						fReset = false;
						continue;
					}
				}

				boolean processed = false;
				try {
					DirtyRegion[] toRefresh = getRequests();
					if (toRefresh != null && toRefresh.length > 0) {
						processed = true;
						beginProcessing();

						for (int i = 0; i < toRefresh.length && fDocument != null; i++) {
							if (fIsCanceled)
								return;
							process(toRefresh[i]);
						}
					}
				}
				finally {
					if (processed)
						endProcessing();
				}
				
			}
		}
	}

	public void setDelay(long delay) {
		fDelay = delay;
	}

	public void setDocument(IDocument doc) {
		if (fDocument != null) {
			// unhook old document listener
			fDocument.removeDocumentListener(fDocumentListener);
			if (fDocument instanceof IDocumentExtension4) {
				((IDocumentExtension4) fDocument).removeDocumentRewriteSessionListener(fDocumentRewriteSessionListener);
			}
		}

		fDocument = doc;
		
		for (IReconcilingStrategy strategy : fReconcilingStrategies.values()) {
			strategy.setDocument(doc);
		}
		
		if (fDocument != null) {
			if (fDocument instanceof IDocumentExtension4) {
				((IDocumentExtension4) fDocument).addDocumentRewriteSessionListener(fDocumentRewriteSessionListener);
			}
			// hook up new document listener
			fDocument.addDocumentListener(fDocumentListener);

			setEntireDocumentDirty(doc);
		}
		
		
	}

	/**
	 * This method is called before the initial reconciling of the document. 
	 */
	protected void initialReconcile() {
		for (IReconcilingStrategy strategy : fReconcilingStrategies.values()) {
			if (strategy instanceof IReconcilingStrategyExtension) {
				((IReconcilingStrategyExtension) strategy).initialReconcile();
			}
		}
	}

	/**
	 * Sets the document partitioning for this reconciler.
	 * 
	 * @param partitioning
	 *            the document partitioning for this reconciler
	 */
	public void setDocumentPartitioning(String partitioning) {
		fPartitioning = partitioning;
	}

	/**
	 * Forces reconciling of the entire document.
	 */
	protected void forceReconciling() {
		if (!fHasReconciled)
			return;

		setEntireDocumentDirty(getDocument());
	}

	/**
	 * Basically means process the entire document.
	 * 
	 * @param document
	 */
	protected void setEntireDocumentDirty(IDocument document) {

		// make the entire document dirty
		// this also happens on a "save as"
		if (document != null && isInstalled()) {

			// since we're marking the entire doc dirty
			flushDirtyRegionQueue();

			/**
			 * https://bugs.eclipse.org/bugs/show_bug.cgi?id=199053
			 * 
			 * Process the strategies for the last known-good partitions to
			 * ensure all problem annotations are cleared if needed.
			 */
			if (fLastPartitions != null && document.getLength() == 0) {
				for (int i = 0; i < fLastPartitions.length; i++) {
					IReconcilingStrategy strategy = getReconcilingStrategy(fLastPartitions[i].getType());
					if (strategy != null) {
						strategy.reconcile(fLastPartitions[i]);
					}
				}
			}
			else {
				DirtyRegion entireDocument = createDirtyRegion(0, document.getLength(), DirtyRegion.INSERT);
				processDirtyRegion(entireDocument);
			}
		}
	}

	/**
	 * @param isInstalled
	 *            The isInstalled to set.
	 */
	void setInstalled(boolean isInstalled) {
		fIsInstalled = isInstalled;
	}

	public void setReconcilingStrategy(String partitionType, IReconcilingStrategy strategy) {
		if (partitionType == null) {
			throw new IllegalArgumentException();
		}

		if (strategy == null) {
			fReconcilingStrategies.remove(partitionType);
		}
		else {
			fReconcilingStrategies.put(partitionType, strategy);
		}
	}

	/**
	 * @see org.eclipse.jface.text.reconciler.IReconciler#uninstall()
	 */
	public void uninstall() {
		if (isInstalled()) {
			// removes widget listener
			getTextViewer().removeTextInputListener(fTextInputListener);
			setInstalled(false);
			fIsCanceled = true;
			synchronized (this) {
				BackgroundThread bt = fThread;
				fThread= null;
				bt.cancel();
			}
		}
		synchronized (fDirtyRegionQueue) {
			fDirtyRegionQueue.clear();
		}
		setDocument(null);
	}

	protected synchronized void startReconciling() {
		if (fThread == null)
			return;
		if (!fThread.isAlive()) {
			try {
				fThread.start();
			} catch (IllegalThreadStateException e) { }
		}
	}
}
