/*******************************************************************************
 * Copyright (c) 2000, 2011 IBM Corporation and others.
 *
 * 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
 *******************************************************************************/
package org.eclipse.jface.text.reconciler;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;

import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.ITextInputListener;
import org.eclipse.jface.text.ITextViewer;


/**
 * Abstract implementation of {@link IReconciler}. The reconciler
 * listens to input document changes as well as changes of
 * the input document of the text viewer it is installed on. Depending on
 * its configuration it manages the received change notifications in a
 * queue folding neighboring or overlapping changes together. The reconciler
 * processes the dirty regions as a background activity after having waited for further
 * changes for the configured duration of time. A reconciler is started using the
 * {@link #install(ITextViewer)} method.  As a first step {@link #initialProcess()} is
 * executed in the background. Then, the reconciling thread waits for changes that
 * need to be reconciled. A reconciler can be resumed by calling {@link #forceReconciling()}
 * independent from the existence of actual changes. This mechanism is for subclasses only.
 * It is the clients responsibility to stop a reconciler using its {@link #uninstall()}
 * method. Unstopped reconcilers do not free their resources.
 * <p>
 * It is subclass responsibility to specify how dirty regions are processed.
 * </p>
 *
 * @see org.eclipse.jface.text.IDocumentListener
 * @see org.eclipse.jface.text.ITextInputListener
 * @see org.eclipse.jface.text.reconciler.DirtyRegion
 * @since 2.0
 */
abstract public class AbstractReconciler implements IReconciler {


	/**
	 * Background thread for the reconciling activity.
	 */
	class BackgroundThread extends Thread {

		/** Has the reconciler been canceled. */
		private boolean fCanceled= false;
		/** Has the reconciler been reset. */
		private boolean fReset= false;
		/** Some changes need to be processed. */
		private boolean fIsDirty= false;
		/** Is a reconciling strategy active. */
		private boolean fIsActive= false;

		/**
		 * Creates a new background thread. The thread
		 * runs with minimal priority.
		 *
		 * @param name the thread's name
		 */
		public BackgroundThread(String name) {
			super(name);
			setPriority(Thread.MIN_PRIORITY);
			setDaemon(true);
		}

		/**
		 * Returns whether a reconciling strategy is active right now.
		 *
		 * @return <code>true</code> if a activity is active
		 */
		public boolean isActive() {
			return fIsActive;
		}

		/**
		 * Returns whether some changes need to be processed.
		 *
		 * @return <code>true</code> if changes wait to be processed
		 * @since 3.0
		 */
		public synchronized boolean isDirty() {
			return fIsDirty;
		}

		/**
		 * Cancels the background thread.
		 */
		public void cancel() {
			fCanceled= true;
			IProgressMonitor pm= fProgressMonitor;
			if (pm != null)
				pm.setCanceled(true);
			synchronized (fDirtyRegionQueue) {
				fDirtyRegionQueue.notifyAll();
			}
		}

		/**
		 * Suspends the caller of this method until this background thread has
		 * emptied the dirty region queue.
		 */
		public void suspendCallerWhileDirty() {
			boolean isDirty;
			do {
				synchronized (fDirtyRegionQueue) {
					isDirty= fDirtyRegionQueue.getSize() > 0;
					if (isDirty) {
						try {
							fDirtyRegionQueue.wait();
						} catch (InterruptedException x) {
						}
					}
				}
			} while (isDirty);
		}

		/**
		 * Reset the background thread as the text viewer has been changed,
		 */
		public void reset() {

			if (fDelay > 0) {

				synchronized (this) {
					fIsDirty= true;
					fReset= true;
				}

			} else {

				synchronized (this) {
					fIsDirty= true;
				}

				synchronized (fDirtyRegionQueue) {
					fDirtyRegionQueue.notifyAll();
				}
			}

			reconcilerReset();
		}

		/**
		 * The background activity. Waits until there is something in the
		 * queue managing the changes that have been applied to the text viewer.
		 * Removes the first change from the queue and process it.
		 * <p>
		 * Calls {@link AbstractReconciler#initialProcess()} on entrance.
		 * </p>
		 */
		@Override
		public void run() {

			synchronized (fDirtyRegionQueue) {
				try {
					fDirtyRegionQueue.wait(fDelay);
				} catch (InterruptedException x) {
				}
			}

			if (fCanceled)
				return;

			initialProcess();

			while (!fCanceled) {

				synchronized (fDirtyRegionQueue) {
					try {
						fDirtyRegionQueue.wait(fDelay);
					} catch (InterruptedException x) {
					}
				}

				if (fCanceled)
					break;

				if (!isDirty())
					continue;

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

				DirtyRegion r= null;
				synchronized (fDirtyRegionQueue) {
					r= fDirtyRegionQueue.removeNextDirtyRegion();
				}

				fIsActive= true;

				fProgressMonitor.setCanceled(false);

				process(r);

				synchronized (fDirtyRegionQueue) {
					if (0 == fDirtyRegionQueue.getSize()) {
						synchronized (this) {
							fIsDirty= fProgressMonitor.isCanceled();
						}
						fDirtyRegionQueue.notifyAll();
					}
				}

				fIsActive= false;
			}
		}
	}

	/**
	 * Internal document listener and text input listener.
	 */
	class Listener implements IDocumentListener, ITextInputListener {

		@Override
		public void documentAboutToBeChanged(DocumentEvent e) {
		}

		@Override
		public void documentChanged(DocumentEvent e) {

			if (fThread.isActive() || !fThread.isDirty() && fThread.isAlive()) {
				if (!fIsAllowedToModifyDocument && Thread.currentThread() == fThread)
					throw new UnsupportedOperationException("The reconciler thread is not allowed to modify the document"); //$NON-NLS-1$
				aboutToBeReconciled();
			}

			/*
			 * The second OR condition handles the case when the document
			 * gets changed while still inside initialProcess().
			 */
			if (fThread.isActive() || fThread.isDirty() && fThread.isAlive())
				fProgressMonitor.setCanceled(true);

			if (fIsIncrementalReconciler)
				createDirtyRegion(e);

			fThread.reset();

		}

		@Override
		public void inputDocumentAboutToBeChanged(IDocument oldInput, IDocument newInput) {

			if (oldInput == fDocument) {

				if (fDocument != null)
					fDocument.removeDocumentListener(this);

				if (fIsIncrementalReconciler) {
					synchronized (fDirtyRegionQueue) {
						fDirtyRegionQueue.purgeQueue();
					}
					if (fDocument != null && fDocument.getLength() > 0 && fThread.isDirty() && fThread.isAlive()) {
						DocumentEvent e= new DocumentEvent(fDocument, 0, fDocument.getLength(), ""); //$NON-NLS-1$
						createDirtyRegion(e);
						fThread.reset();
						fThread.suspendCallerWhileDirty();
					}
				}

				fDocument= null;
			}
		}

		@Override
		public void inputDocumentChanged(IDocument oldInput, IDocument newInput) {

			fDocument= newInput;
			if (fDocument == null)
				return;


			reconcilerDocumentChanged(fDocument);

			fDocument.addDocumentListener(this);

			if (!fThread.isDirty())
				aboutToBeReconciled();

			startReconciling();
		}
	}

	/** Queue to manage the changes applied to the text viewer. */
	private DirtyRegionQueue fDirtyRegionQueue;
	/** The background thread. */
	private BackgroundThread fThread;
	/** Internal document and text input listener. */
	private Listener fListener;
	/** The background thread delay. */
	private int fDelay= 500;
	/** Are there incremental reconciling strategies? */
	private boolean fIsIncrementalReconciler= true;
	/** The progress monitor used by this reconciler. */
	private IProgressMonitor fProgressMonitor;
	/**
	 * Tells whether this reconciler is allowed to modify the document.
	 * @since 3.2
	 */
	private boolean fIsAllowedToModifyDocument= true;


	/** The text viewer's document. */
	private IDocument fDocument;
	/** The text viewer */
	private ITextViewer fViewer;


	/**
	 * Processes a dirty region. If the dirty region is <code>null</code> the whole
	 * document is consider being dirty. The dirty region is partitioned by the
	 * document and each partition is handed over to a reconciling strategy registered
	 * for the partition's content type.
	 *
	 * @param dirtyRegion the dirty region to be processed
	 */
	abstract protected void process(DirtyRegion dirtyRegion);

	/**
	 * Hook called when the document whose contents should be reconciled
	 * has been changed, i.e., the input document of the text viewer this
	 * reconciler is installed on. Usually, subclasses use this hook to
	 * inform all their reconciling strategies about the change.
	 *
	 * @param newDocument the new reconciler document
	 */
	abstract protected void reconcilerDocumentChanged(IDocument newDocument);


	/**
	 * Creates a new reconciler without configuring it.
	 */
	protected AbstractReconciler() {
		fProgressMonitor= new NullProgressMonitor();
	}

	/**
	 * Tells the reconciler how long it should wait for further text changes before
	 * activating the appropriate reconciling strategies.
	 *
	 * @param delay the duration in milliseconds of a change collection period.
	 */
	public void setDelay(int delay) {
		fDelay= delay;
	}

	/**
	 * Tells the reconciler whether any of the available reconciling strategies
	 * is interested in getting detailed dirty region information or just in the
	 * fact that the document has been changed. In the second case, the reconciling
	 * can not incrementally be pursued.
	 *
	 * @param isIncremental indicates whether this reconciler will be configured with
	 *		incremental reconciling strategies
	 *
	 * @see DirtyRegion
	 * @see IReconcilingStrategy
	 */
	public void setIsIncrementalReconciler(boolean isIncremental) {
		fIsIncrementalReconciler= isIncremental;
	}

	/**
	 * Tells the reconciler whether it is allowed to change the document
	 * inside its reconciler thread.
	 * <p>
	 * If this is set to <code>false</code> an {@link UnsupportedOperationException}
	 * will be thrown when this restriction will be violated.
	 * </p>
	 *
	 * @param isAllowedToModify indicates whether this reconciler is allowed to modify the document
	 * @since 3.2
	 */
	public void setIsAllowedToModifyDocument(boolean isAllowedToModify) {
		fIsAllowedToModifyDocument= isAllowedToModify;
	}

	/**
	 * Sets the progress monitor of this reconciler.
	 *
	 * @param monitor the monitor to be used
	 */
	public void setProgressMonitor(IProgressMonitor monitor) {
		Assert.isLegal(monitor != null);
		fProgressMonitor= monitor;
	}

	/**
	 * Returns whether any of the reconciling strategies is interested in
	 * detailed dirty region information.
	 *
	 * @return whether this reconciler is incremental
	 *
	 * @see IReconcilingStrategy
	 */
	protected boolean isIncrementalReconciler() {
		return fIsIncrementalReconciler;
	}

	/**
	 * Returns the input document of the text viewer this reconciler is installed on.
	 *
	 * @return the reconciler document
	 */
	protected IDocument getDocument() {
		return fDocument;
	}

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

	/**
	 * Returns the progress monitor of this reconciler.
	 *
	 * @return the progress monitor of this reconciler
	 */
	protected IProgressMonitor getProgressMonitor() {
		return fProgressMonitor;
	}

	@Override
	public void install(ITextViewer textViewer) {

		Assert.isNotNull(textViewer);
		fViewer= textViewer;

		synchronized (this) {
			if (fThread != null)
				return;
			fThread= new BackgroundThread(getClass().getName());
		}

		fDirtyRegionQueue= new DirtyRegionQueue();

		fListener= new Listener();
		fViewer.addTextInputListener(fListener);

		// see bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=67046
		// if the reconciler gets installed on a viewer that already has a document
		// (e.g. when reusing editors), we force the listener to register
		// itself as document listener, because there will be no input change
		// on the viewer.
		// In order to do that, we simulate an input change.
		IDocument document= textViewer.getDocument();
		if (document != null) {
			fListener.inputDocumentAboutToBeChanged(fDocument, document);
			fListener.inputDocumentChanged(fDocument, document);
		}
	}

	@Override
	public void uninstall() {
		if (fListener != null) {

			fViewer.removeTextInputListener(fListener);
			if (fDocument != null) {
				fListener.inputDocumentAboutToBeChanged(fDocument, null);
				fListener.inputDocumentChanged(fDocument, null);
			}
			fListener= null;

			synchronized (this) {
				// http://dev.eclipse.org/bugs/show_bug.cgi?id=19135
				BackgroundThread bt= fThread;
				fThread= null;
				bt.cancel();
			}
		}
	}

	/**
	 * Creates a dirty region for a document event and adds it to the queue.
	 *
	 * @param e the document event for which to create a dirty region
	 */
	private void createDirtyRegion(DocumentEvent e) {
		synchronized (fDirtyRegionQueue) {
			if (e.getLength() == 0 && e.getText() != null) {
				// Insert
				fDirtyRegionQueue.addDirtyRegion(new DirtyRegion(e.getOffset(), e.getText().length(), DirtyRegion.INSERT, e.getText()));

			} else if (e.getText() == null || e.getText().length() == 0) {
				// Remove
				fDirtyRegionQueue.addDirtyRegion(new DirtyRegion(e.getOffset(), e.getLength(), DirtyRegion.REMOVE, null));

			} else {
				// Replace (Remove + Insert)
				fDirtyRegionQueue.addDirtyRegion(new DirtyRegion(e.getOffset(), e.getLength(), DirtyRegion.REMOVE, null));
				fDirtyRegionQueue.addDirtyRegion(new DirtyRegion(e.getOffset(), e.getText().length(), DirtyRegion.INSERT, e.getText()));
			}
		}
	}

	/**
	 * Hook for subclasses which want to perform some
	 * action as soon as reconciliation is needed.
	 * <p>
	 * Default implementation is to do nothing.
	 * </p>
	 *
	 * @since 3.0
	 */
	protected void aboutToBeReconciled() {
	}

	/**
	 * This method is called on startup of the background activity. It is called only
	 * once during the life time of the reconciler. Clients may reimplement this method.
	 */
	protected void initialProcess() {
	}

	/**
	 * Forces the reconciler to reconcile the structure of the whole document.
	 * Clients may extend this method.
	 */
	protected void forceReconciling() {

		if (fDocument != null) {

			if (!fThread.isDirty()&& fThread.isAlive())
				aboutToBeReconciled();

			if (fThread.isActive())
				fProgressMonitor.setCanceled(true);

			if (fIsIncrementalReconciler) {
				DocumentEvent e= new DocumentEvent(fDocument, 0, fDocument.getLength(), fDocument.get());
				createDirtyRegion(e);
			}

			startReconciling();
		}
	}

	/**
	 * Starts the reconciler to reconcile the queued dirty-regions.
	 * Clients may extend this method.
	 */
	protected synchronized void startReconciling() {
		if (fThread == null)
			return;

		if (!fThread.isAlive()) {
			try {
				fThread.start();
			} catch (IllegalThreadStateException e) {
				// see https://bugs.eclipse.org/bugs/show_bug.cgi?id=40549
				// This is the only instance where the thread is started; since
				// we checked that it is not alive, it must be dead already due
				// to a run-time exception or error. Exit.
			}
		} else {
			fThread.reset();
		}
	}

	/**
	 * Hook that is called after the reconciler thread has been reset.
	 */
	protected void reconcilerReset() {
	}

	/**
	 * Tells whether the code is running in this reconciler's
	 * background thread.
	 *
	 * @return <code>true</code> if running in this reconciler's background thread
	 * @since 3.4
	 */
	protected boolean isRunningInReconcilerThread() {
		return Thread.currentThread() == fThread;
	}
}
