/*******************************************************************************
 * Copyright (c) 2000, 2016 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;


import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.regex.PatternSyntaxException;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.SafeRunner;


/**
 * Abstract default implementation of <code>IDocument</code> and its extension
 * interfaces {@link org.eclipse.jface.text.IDocumentExtension},
 * {@link org.eclipse.jface.text.IDocumentExtension2},
 * {@link org.eclipse.jface.text.IDocumentExtension3},
 * {@link org.eclipse.jface.text.IDocumentExtension4}, as well as
 * {@link org.eclipse.jface.text.IRepairableDocument}.
 * <p>
 *
 * An <code>AbstractDocument</code> supports the following implementation
 * plug-ins:
 * <ul>
 * <li>a text store implementing {@link org.eclipse.jface.text.ITextStore} for
 *     storing and managing the document's content,</li>
 * <li>a line tracker implementing {@link org.eclipse.jface.text.ILineTracker}
 *     to map character positions to line numbers and vice versa</li>
 * </ul>
 * The document can dynamically change the text store when switching between
 * sequential rewrite mode and normal mode.
 * <p>
 *
 * This class must be subclassed. Subclasses must configure which implementation
 * plug-ins the document instance should use. Subclasses are not intended to
 * overwrite existing methods.
 *
 * @see org.eclipse.jface.text.ITextStore
 * @see org.eclipse.jface.text.ILineTracker
 */
public abstract class AbstractDocument implements IDocument, IDocumentExtension, IDocumentExtension2, IDocumentExtension3, IDocumentExtension4, IRepairableDocument, IRepairableDocumentExtension {

	/**
	 * Tells whether this class is in debug mode.
	 * @since 3.1
	 */
	private static final boolean DEBUG= false;


	/**
	 * Inner class to bundle a registered post notification replace operation together with its
	 * owner.
	 *
	 * @since 2.0
	 */
	static private class RegisteredReplace {
		/** The owner of this replace operation. */
		IDocumentListener fOwner;
		/** The replace operation */
		IDocumentExtension.IReplace fReplace;

		/**
		 * Creates a new bundle object.
		 * @param owner the document listener owning the replace operation
		 * @param replace the replace operation
		 */
		RegisteredReplace(IDocumentListener owner, IDocumentExtension.IReplace replace) {
			fOwner= owner;
			fReplace= replace;
		}
	}


	/** The document's text store */
	private ITextStore   fStore;
	/** The document's line tracker */
	private ILineTracker fTracker;
	/** The registered document listeners */
	private ListenerList<IDocumentListener> fDocumentListeners;
	/** The registered pre-notified document listeners */
	private ListenerList<IDocumentListener> fPrenotifiedDocumentListeners;
	/** The registered document partitioning listeners */
	private ListenerList<IDocumentPartitioningListener> fDocumentPartitioningListeners;
	/** All positions managed by the document ordered by their start positions. */
	private Map<String, List<Position>> fPositions;
	/**
	 * All positions managed by the document ordered by their end positions.
	 * @since 3.4
	 */
	private Map<String, List<Position>> fEndPositions;
	/** All registered document position updaters */
	private List<IPositionUpdater> fPositionUpdaters;
	/**
	 * The list of post notification changes
	 * @since 2.0
	 */
	private List<RegisteredReplace> fPostNotificationChanges;
	/**
	 * The reentrance count for post notification changes.
	 * @since 2.0
	 */
	private int fReentranceCount= 0;
	/**
	 * Indicates whether post notification change processing has been stopped.
	 * @since 2.0
	 */
	private int fStoppedCount= 0;
	/**
	 * Indicates whether the registration of post notification changes should be ignored.
	 * @since 2.1
	 */
	private boolean fAcceptPostNotificationReplaces= true;
	/**
	 * Indicates whether the notification of listeners has been stopped.
	 * @since 2.1
	 */
	private int fStoppedListenerNotification= 0;
	/**
	 * The document event to be sent after listener notification has been resumed.
	 * @since 2.1
	 */
	private DocumentEvent fDeferredDocumentEvent;
	/**
	 * The registered document partitioners.
	 * @since 3.0
	 */
	private Map<String, IDocumentPartitioner> fDocumentPartitioners;
	/**
	 * The partitioning changed event.
	 * @since 3.0
	 */
	private DocumentPartitioningChangedEvent fDocumentPartitioningChangedEvent;
	/**
	 * The find/replace document adapter.
	 * @since 3.0
	 */
	private FindReplaceDocumentAdapter fFindReplaceDocumentAdapter;
	/**
	 * The active document rewrite session.
	 * @since 3.1
	 */
	private DocumentRewriteSession fDocumentRewriteSession;
	/**
	 * The registered document rewrite session listeners.
	 * @since 3.1
	 */
	private List<IDocumentRewriteSessionListener> fDocumentRewriteSessionListeners;
	/**
	 * The current modification stamp.
	 * @since 3.1
	 */
	private long fModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
	/**
	 * Keeps track of next modification stamp.
	 * @since 3.1.1
	 */
	private long fNextModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
	/**
	 * This document's default line delimiter.
	 * @since 3.1
	 */
	private String fInitialLineDelimiter;


	/**
	 * The default constructor does not perform any configuration
	 * but leaves it to the clients who must first initialize the
	 * implementation plug-ins and then call <code>completeInitialization</code>.
	 * Results in the construction of an empty document.
	 */
	protected AbstractDocument() {
		fModificationStamp= getNextModificationStamp();
	}


	/**
	 * Returns the document's text store. Assumes that the
	 * document has been initialized with a text store.
	 *
	 * @return the document's text store
	 */
	protected ITextStore getStore() {
		Assert.isNotNull(fStore);
		return fStore;
	}

	/**
	 * Returns the document's line tracker. Assumes that the
	 * document has been initialized with a line tracker.
	 *
	 * @return the document's line tracker
	 */
	protected ILineTracker getTracker() {
		Assert.isNotNull(fTracker);
		return fTracker;
	}

	private static <T> List<T> asList(ListenerList<T> listenerList) {
		List<?> list= Arrays.asList(listenerList.getListeners());
		@SuppressWarnings("unchecked")
		List<T> castList= (List<T>) list;
		return castList;
	}


	/**
	 * Returns the document's document listeners.
	 *
	 * @return the document's document listeners
	 */
	protected List<IDocumentListener> getDocumentListeners() {
		return asList(fDocumentListeners);
	}

	/**
	 * Returns the document's partitioning listeners.
	 *
	 * @return the document's partitioning listeners
	 */
	protected List<IDocumentPartitioningListener> getDocumentPartitioningListeners() {
		return asList(fDocumentPartitioningListeners);
	}

	/**
	 * Returns all positions managed by the document grouped by category.
	 *
	 * @return the document's positions
	 */
	protected Map<String, List<Position>> getDocumentManagedPositions() {
		return fPositions;
	}

	@Override
	public IDocumentPartitioner getDocumentPartitioner() {
		return getDocumentPartitioner(DEFAULT_PARTITIONING);
	}



	//--- implementation configuration interface ------------

	/**
	 * Sets the document's text store.
	 * Must be called at the beginning of the constructor.
	 *
	 * @param store the document's text store
	 */
	protected void setTextStore(ITextStore store) {
		fStore= store;
	}

	/**
	 * Sets the document's line tracker.
	 * Must be called at the beginning of the constructor.
	 *
	 * @param tracker the document's line tracker
	 */
	protected void setLineTracker(ILineTracker tracker) {
		fTracker= tracker;
	}

	@Override
	public void setDocumentPartitioner(IDocumentPartitioner partitioner) {
		setDocumentPartitioner(DEFAULT_PARTITIONING, partitioner);
	}

	/**
	 * Initializes document listeners, positions, and position updaters.
	 * Must be called inside the constructor after the implementation plug-ins
	 * have been set.
	 */
	protected void completeInitialization() {

		fPositions= new HashMap<>();
		fEndPositions= new HashMap<>();
		fPositionUpdaters= new CopyOnWriteArrayList<>();
		fDocumentListeners= new ListenerList<>(ListenerList.IDENTITY);
		fPrenotifiedDocumentListeners= new ListenerList<>(ListenerList.IDENTITY);
		fDocumentPartitioningListeners= new ListenerList<>(ListenerList.IDENTITY);
		fDocumentRewriteSessionListeners= new ArrayList<>();

		addPositionCategory(DEFAULT_CATEGORY);
		addPositionUpdater(new DefaultPositionUpdater(DEFAULT_CATEGORY));
	}


	//-------------------------------------------------------

	@Override
	public void addDocumentListener(IDocumentListener listener) {
		Assert.isNotNull(listener);
		fDocumentListeners.add(listener);
	}

	@Override
	public void removeDocumentListener(IDocumentListener listener) {
		Assert.isNotNull(listener);
		fDocumentListeners.remove(listener);
	}

	@Override
	public void addPrenotifiedDocumentListener(IDocumentListener listener) {
		Assert.isNotNull(listener);
		fPrenotifiedDocumentListeners.add(listener);
	}

	@Override
	public void removePrenotifiedDocumentListener(IDocumentListener listener) {
		Assert.isNotNull(listener);
		fPrenotifiedDocumentListeners.remove(listener);
	}

	@Override
	public void addDocumentPartitioningListener(IDocumentPartitioningListener listener) {
		Assert.isNotNull(listener);
		fDocumentPartitioningListeners.add(listener);
	}

	@Override
	public void removeDocumentPartitioningListener(IDocumentPartitioningListener listener) {
		Assert.isNotNull(listener);
		fDocumentPartitioningListeners.remove(listener);
	}

	@Override
	public void addPosition(String category, Position position) throws BadLocationException, BadPositionCategoryException  {

		if ((0 > position.offset) || (0 > position.length) || (position.offset + position.length > getLength()))
			throw new BadLocationException();

		if (category == null)
			throw new BadPositionCategoryException();

		List<Position> list= fPositions.get(category);
		if (list == null)
			throw new BadPositionCategoryException();
		list.add(computeIndexInPositionList(list, position.offset), position);

		List<Position> endPositions= fEndPositions.get(category);
		if (endPositions == null)
			throw new BadPositionCategoryException();
		endPositions.add(computeIndexInPositionList(endPositions, position.offset + position.length - 1, false), position);
	}

	@Override
	public void addPosition(Position position) throws BadLocationException {
		try {
			addPosition(DEFAULT_CATEGORY, position);
		} catch (BadPositionCategoryException e) {
		}
	}

	@Override
	public void addPositionCategory(String category) {

		if (category == null)
			return;

		if (!containsPositionCategory(category)) {
			fPositions.put(category, new ArrayList<>());
			fEndPositions.put(category, new ArrayList<>());
		}
	}

	@Override
	public void addPositionUpdater(IPositionUpdater updater) {
		insertPositionUpdater(updater, fPositionUpdaters.size());
	}

	@Override
	public boolean containsPosition(String category, int offset, int length) {

		if (category == null)
			return false;

		List<Position> list= fPositions.get(category);
		if (list == null)
			return false;

		int size= list.size();
		if (size == 0)
			return false;

		int index= computeIndexInPositionList(list, offset);
		if (index < size) {
			Position p= list.get(index);
			while (p != null && p.offset == offset) {
				if (p.length == length)
					return true;
				++ index;
				p= (index < size) ? list.get(index) : null;
			}
		}

		return false;
	}

	@Override
	public boolean containsPositionCategory(String category) {
		if (category != null)
			return fPositions.containsKey(category);
		return false;
	}


	/**
	 * Computes the index in the list of positions at which a position with the given
	 * offset would be inserted. The position is supposed to become the first in this list
	 * of all positions with the same offset.
	 *
	 * @param positions the list in which the index is computed
	 * @param offset the offset for which the index is computed
	 * @return the computed index
	 *
	 * @see IDocument#computeIndexInCategory(String, int)
	 * @deprecated As of 3.4, replaced by {@link #computeIndexInPositionList(List, int, boolean)}
	 */
	@Deprecated
	protected int computeIndexInPositionList(List<? extends Position> positions, int offset) {
		return computeIndexInPositionList(positions, offset, true);
	}

	/**
	 * Computes the index in the list of positions at which a position with the given
	 * position would be inserted. The position to insert is supposed to become the first
	 * in this list of all positions with the same position.
	 *
	 * @param positions the list in which the index is computed
	 * @param offset the offset for which the index is computed
	 * @param orderedByOffset <code>true</code> if ordered by offset, false if ordered by end position
	 * @return the computed index
	 * @since 3.4
	 */
	protected int computeIndexInPositionList(List<? extends Position> positions, int offset, boolean orderedByOffset) {
		if (positions.isEmpty())
			return 0;

		int left= 0;
		int right= positions.size() -1;
		int mid= 0;
		Position p= null;

		while (left < right) {

			mid= (left + right) / 2;

			p= positions.get(mid);
			int pOffset= getOffset(orderedByOffset, p);
			if (offset < pOffset) {
				if (left == mid)
					right= left;
				else
					right= mid -1;
			} else if (offset > pOffset) {
				if (right == mid)
					left= right;
				else
					left= mid  +1;
			} else if (offset == pOffset) {
				left= right= mid;
			}

		}

		int pos= left;
		p= positions.get(pos);
		int pPosition= getOffset(orderedByOffset, p);
		if (offset > pPosition) {
			// append to the end
			pos++;
		} else {
			// entry will become the first of all entries with the same offset
			do {
				--pos;
				if (pos < 0)
					break;
				p= positions.get(pos);
				pPosition= getOffset(orderedByOffset, p);
			} while (offset == pPosition);
			++pos;
		}

		Assert.isTrue(0 <= pos && pos <= positions.size());

		return pos;
	}

	/*
	 * @since 3.4
	 */
	private int getOffset(boolean orderedByOffset, Position position) {
		if (orderedByOffset || position.getLength() == 0)
			return position.getOffset();
		return position.getOffset() + position.getLength() - 1;
	}

	@Override
	public int computeIndexInCategory(String category, int offset) throws BadLocationException, BadPositionCategoryException {

		if (0 > offset || offset > getLength())
			throw new BadLocationException();

		List<Position> c= fPositions.get(category);
		if (c == null)
			throw new BadPositionCategoryException();

		return computeIndexInPositionList(c, offset);
	}

	/**
	 * Fires the document partitioning changed notification to all registered
	 * document partitioning listeners. Uses a robust iterator.
	 *
	 * @deprecated as of 2.0. Use <code>fireDocumentPartitioningChanged(IRegion)</code> instead.
	 */
	@Deprecated
	protected void fireDocumentPartitioningChanged() {
		if (fDocumentPartitioningListeners == null)
			return;

		for (IDocumentPartitioningListener listener : fDocumentPartitioningListeners) {
			listener.documentPartitioningChanged(this);
		}
	}

	/**
	 * Fires the document partitioning changed notification to all registered
	 * document partitioning listeners. Uses a robust iterator.
	 *
	 * @param region the region in which partitioning has changed
	 *
	 * @see IDocumentPartitioningListenerExtension
	 * @since 2.0
	 * @deprecated as of 3.0. Use
	 *             <code>fireDocumentPartitioningChanged(DocumentPartitioningChangedEvent)</code>
	 *             instead.
	 */
	@Deprecated
	protected void fireDocumentPartitioningChanged(IRegion region) {
		if (fDocumentPartitioningListeners == null)
			return;

		for (IDocumentPartitioningListener l : fDocumentPartitioningListeners) {
			try {
				if (l instanceof IDocumentPartitioningListenerExtension)
					((IDocumentPartitioningListenerExtension)l).documentPartitioningChanged(this, region);
				else
					l.documentPartitioningChanged(this);
			} catch (Exception ex) {
				log(ex);
			}
		}
	}

	/**
	 * Fires the document partitioning changed notification to all registered
	 * document partitioning listeners. Uses a robust iterator.
	 *
	 * @param event the document partitioning changed event
	 *
	 * @see IDocumentPartitioningListenerExtension2
	 * @since 3.0
	 */
	protected void fireDocumentPartitioningChanged(DocumentPartitioningChangedEvent event) {
		if (fDocumentPartitioningListeners == null)
			return;

		for (IDocumentPartitioningListener l : fDocumentPartitioningListeners) {
			try {
				if (l instanceof IDocumentPartitioningListenerExtension2) {
					IDocumentPartitioningListenerExtension2 extension2= (IDocumentPartitioningListenerExtension2)l;
					extension2.documentPartitioningChanged(event);
				} else if (l instanceof IDocumentPartitioningListenerExtension) {
					IDocumentPartitioningListenerExtension extension= (IDocumentPartitioningListenerExtension)l;
					extension.documentPartitioningChanged(this, event.getCoverage());
				} else {
					l.documentPartitioningChanged(this);
				}
			} catch (Exception ex) {
				log(ex);
			}
		}
	}

	/**
	 * Fires the given document event to all registers document listeners informing them
	 * about the forthcoming document manipulation. Uses a robust iterator.
	 *
	 * @param event the event to be sent out
	 */
	protected void fireDocumentAboutToBeChanged(DocumentEvent event) {

		// IDocumentExtension
		if (fReentranceCount == 0)
			flushPostNotificationChanges();

		if (fDocumentPartitioners != null) {
			Iterator<IDocumentPartitioner> e= fDocumentPartitioners.values().iterator();
			while (e.hasNext()) {
				IDocumentPartitioner p= e.next();
				if (p instanceof IDocumentPartitionerExtension3) {
					IDocumentPartitionerExtension3 extension= (IDocumentPartitionerExtension3) p;
					if (extension.getActiveRewriteSession() != null)
						continue;
				}
				try {
					p.documentAboutToBeChanged(event);
				} catch (Exception ex) {
					log(ex);
				}
			}
		}

		for (IDocumentListener listener : fPrenotifiedDocumentListeners) {
			try {
				listener.documentAboutToBeChanged(event);
			} catch (Exception ex) {
				log(ex);
			}
		}

		for (IDocumentListener listener : fDocumentListeners) {
			try {
				listener.documentAboutToBeChanged(event);
			} catch (Exception ex) {
				log(ex);
			}
		}

	}

	/**
	 * Updates document partitioning and document positions according to the
	 * specification given by the document event.
	 *
	 * @param event the document event describing the change to which structures must be adapted
	 */
	protected void updateDocumentStructures(DocumentEvent event) {

		if (fDocumentPartitioners != null) {
			fDocumentPartitioningChangedEvent= new DocumentPartitioningChangedEvent(this);
			for (Entry<String, IDocumentPartitioner> entry : fDocumentPartitioners.entrySet()) {

				String partitioning= entry.getKey();
				IDocumentPartitioner partitioner= entry.getValue();

				if (partitioner instanceof IDocumentPartitionerExtension3) {
					IDocumentPartitionerExtension3 extension= (IDocumentPartitionerExtension3) partitioner;
					if (extension.getActiveRewriteSession() != null)
						continue;
				}

				if (partitioner instanceof IDocumentPartitionerExtension) {
					IDocumentPartitionerExtension extension= (IDocumentPartitionerExtension) partitioner;
					IRegion r= extension.documentChanged2(event);
					if (r != null)
						fDocumentPartitioningChangedEvent.setPartitionChange(partitioning, r.getOffset(), r.getLength());
				} else {
					if (partitioner.documentChanged(event))
						fDocumentPartitioningChangedEvent.setPartitionChange(partitioning, 0, event.getDocument().getLength());
				}
			}
		}

		if (!fPositions.isEmpty())
			updatePositions(event);
	}

	/**
	 * Notifies all listeners about the given document change. Uses a robust
	 * iterator.
	 * <p>
	 * Executes all registered post notification replace operation.
	 *
	 * @param event the event to be sent out.
	 */
	protected void doFireDocumentChanged(DocumentEvent event) {
		boolean changed= fDocumentPartitioningChangedEvent != null && !fDocumentPartitioningChangedEvent.isEmpty();
		IRegion change= changed ? fDocumentPartitioningChangedEvent.getCoverage() : null;
		doFireDocumentChanged(event, changed, change);
	}

	/**
	 * Notifies all listeners about the given document change.
	 * Uses a robust iterator. <p>
	 * Executes all registered post notification replace operation.
	 *
	 * @param event the event to be sent out
	 * @param firePartitionChange <code>true</code> if a partition change notification should be sent
	 * @param partitionChange the region whose partitioning changed
	 * @since 2.0
	 * @deprecated as of 3.0. Use <code>doFireDocumentChanged2(DocumentEvent)</code> instead; this method will be removed.
	 */
	@Deprecated
	protected void doFireDocumentChanged(DocumentEvent event, boolean firePartitionChange, IRegion partitionChange) {
		doFireDocumentChanged2(event);
	}

	/**
	 * Notifies all listeners about the given document change. Uses a robust
	 * iterator.
	 * <p>
	 * Executes all registered post notification replace operation.
	 * <p>
	 * This method will be renamed to <code>doFireDocumentChanged</code>.
	 *
	 * @param event the event to be sent out
	 * @since 3.0
	 */
	protected void doFireDocumentChanged2(DocumentEvent event) {

		DocumentPartitioningChangedEvent p= fDocumentPartitioningChangedEvent;
		fDocumentPartitioningChangedEvent= null;
		if (p != null && !p.isEmpty())
			fireDocumentPartitioningChanged(p);

		for (IDocumentListener listener : fPrenotifiedDocumentListeners) {
			try {
				listener.documentChanged(event);
			} catch (Exception ex) {
				log(ex);
			}
		}

		for (IDocumentListener listener : fDocumentListeners) {
			try {
				listener.documentChanged(event);
			} catch (Exception ex) {
				log(ex);
			}
		}

		// IDocumentExtension
		++ fReentranceCount;
		try {
			if (fReentranceCount == 1)
				executePostNotificationChanges();
		} finally {
			-- fReentranceCount;
		}
	}

	/**
	 * Updates the internal document structures and informs all document listeners
	 * if listener notification has been enabled. Otherwise it remembers the event
	 * to be sent to the listeners on resume.
	 *
	 * @param event the document event to be sent out
	 */
	protected void fireDocumentChanged(DocumentEvent event) {
		updateDocumentStructures(event);

		if (fStoppedListenerNotification == 0)
			doFireDocumentChanged(event);
		else
			fDeferredDocumentEvent= event;
	}

	@Override
	public char getChar(int pos) throws BadLocationException {
		if ((0 > pos) || (pos >= getLength()))
			throw new BadLocationException();
		return getStore().get(pos);
	}

	@Override
	public String getContentType(int offset) throws BadLocationException {
		String contentType= null;
		try {
			contentType= getContentType(DEFAULT_PARTITIONING, offset, false);
			Assert.isNotNull(contentType);
		} catch (BadPartitioningException e) {
			Assert.isTrue(false);
		}
		return contentType;
	}

	@Override
	public String[] getLegalContentTypes() {
		String[] contentTypes= null;
		try {
			contentTypes= getLegalContentTypes(DEFAULT_PARTITIONING);
			Assert.isNotNull(contentTypes);
		} catch (BadPartitioningException e) {
			Assert.isTrue(false);
		}
		return contentTypes;
	}

	@Override
	public int getLength() {
		return getStore().getLength();
	}

	@Override
	public String getLineDelimiter(int line) throws BadLocationException {
		return getTracker().getLineDelimiter(line);
	}

	@Override
	public String[] getLegalLineDelimiters() {
		return getTracker().getLegalLineDelimiters();
	}

	@Override
	public String getDefaultLineDelimiter() {

		String lineDelimiter= null;

		try {
			lineDelimiter= getLineDelimiter(0);
		} catch (BadLocationException x) {
		}

		if (lineDelimiter != null)
			return lineDelimiter;

		if (fInitialLineDelimiter != null)
			return fInitialLineDelimiter;

		String sysLineDelimiter= System.lineSeparator();
		String[] delimiters= getLegalLineDelimiters();
		Assert.isTrue(delimiters.length > 0);
		for (String delimiter : delimiters) {
			if (delimiter.equals(sysLineDelimiter)) {
				lineDelimiter= sysLineDelimiter;
				break;
			}
		}

		if (lineDelimiter == null)
			lineDelimiter= delimiters[0];

		return lineDelimiter;

	}

	@Override
	public void setInitialLineDelimiter(String lineDelimiter) {
		Assert.isNotNull(lineDelimiter);
		fInitialLineDelimiter= lineDelimiter;
	}

	@Override
	public int getLineLength(int line) throws BadLocationException {
		return getTracker().getLineLength(line);
	}

	@Override
	public int getLineOfOffset(int pos) throws BadLocationException {
		return getTracker().getLineNumberOfOffset(pos);
	}

	@Override
	public int getLineOffset(int line) throws BadLocationException {
		return getTracker().getLineOffset(line);
	}

	@Override
	public IRegion getLineInformation(int line) throws BadLocationException {
		return getTracker().getLineInformation(line);
	}

	@Override
	public IRegion getLineInformationOfOffset(int offset) throws BadLocationException {
		return getTracker().getLineInformationOfOffset(offset);
	}

	@Override
	public int getNumberOfLines() {
		return getTracker().getNumberOfLines();
	}

	@Override
	public int getNumberOfLines(int offset, int length) throws BadLocationException {
		return getTracker().getNumberOfLines(offset, length);
	}

	@Override
	public int computeNumberOfLines(String text) {
		return getTracker().computeNumberOfLines(text);
	}

	@Override
	public ITypedRegion getPartition(int offset) throws BadLocationException {
		ITypedRegion partition= null;
		try {
			partition= getPartition(DEFAULT_PARTITIONING, offset, false);
			Assert.isNotNull(partition);
		} catch (BadPartitioningException e) {
			Assert.isTrue(false);
		}
		return  partition;
	}

	@Override
	public ITypedRegion[] computePartitioning(int offset, int length) throws BadLocationException {
		ITypedRegion[] partitioning= null;
		try {
			partitioning= computePartitioning(DEFAULT_PARTITIONING, offset, length, false);
			Assert.isNotNull(partitioning);
		} catch (BadPartitioningException e) {
			Assert.isTrue(false);
		}
		return partitioning;
	}

	@Override
	public Position[] getPositions(String category) throws BadPositionCategoryException {

		if (category == null)
			throw new BadPositionCategoryException();

		List<Position> c= fPositions.get(category);
		if (c == null)
			throw new BadPositionCategoryException();

		Position[] positions= new Position[c.size()];
		c.toArray(positions);
		return positions;
	}

	@Override
	public String[] getPositionCategories() {
		String[] categories= new String[fPositions.size()];
		Iterator<String> keys= fPositions.keySet().iterator();
		for (int i= 0; i < categories.length; i++)
			categories[i]= keys.next();
		return categories;
	}

	@Override
	public IPositionUpdater[] getPositionUpdaters() {
		return fPositionUpdaters.toArray(new IPositionUpdater[fPositionUpdaters.size()]);
	}

	@Override
	public String get() {
		return getStore().get(0, getLength());
	}

	@Override
	public String get(int pos, int length) throws BadLocationException {
		int myLength= getLength();
		if ((0 > pos) || (0 > length) || (pos + length > myLength))
			throw new BadLocationException();
		return getStore().get(pos, length);
	}

	@Override
	public void insertPositionUpdater(IPositionUpdater updater, int index) {
		for (IPositionUpdater u: fPositionUpdaters) {
			if (u == updater) {
				return;
			}
		}
		fPositionUpdaters.add(index, updater);
	}

	@Override
	public void removePosition(String category, Position position) throws BadPositionCategoryException {

		if (position == null)
			return;

		if (category == null)
			throw new BadPositionCategoryException();

		List<Position> c= fPositions.get(category);
		if (c == null)
			throw new BadPositionCategoryException();
		removeFromPositionsList(c, position, true);

		List<Position> endPositions= fEndPositions.get(category);
		if (endPositions == null)
			throw new BadPositionCategoryException();
		removeFromPositionsList(endPositions, position, false);
	}

	/**
	 * Remove the given position form the given list of positions based on identity not equality.
	 *
	 * @param positions a list of positions
	 * @param position the position to remove
	 * @param orderedByOffset true if <code>positions</code> is ordered by offset, false if ordered by end position
	 * @since 3.4
	 */
	private void removeFromPositionsList(List<Position> positions, Position position, boolean orderedByOffset) {
		int size= positions.size();

		//Assume position is somewhere near it was before
		int index= computeIndexInPositionList(positions, orderedByOffset ? position.offset : position.offset + position.length - 1, orderedByOffset);
		if (index < size && positions.get(index) == position) {
			positions.remove(index);
			return;
		}

		int back= index - 1;
		int forth= index + 1;
		while (back >= 0 || forth < size) {
			if (back >= 0) {
				if (position == positions.get(back)) {
					positions.remove(back);
					return;
				}
				back--;
			}

			if (forth < size) {
				if (position == positions.get(forth)) {
					positions.remove(forth);
					return;
				}
				forth++;
			}
		}
	}

	@Override
	public void removePosition(Position position) {
		try {
			removePosition(DEFAULT_CATEGORY, position);
		} catch (BadPositionCategoryException e) {
		}
	}

	@Override
	public void removePositionCategory(String category) throws BadPositionCategoryException {

		if (category == null)
			return;

		if ( !containsPositionCategory(category))
			throw new BadPositionCategoryException();

		fPositions.remove(category);
		fEndPositions.remove(category);
	}

	@Override
	public void removePositionUpdater(IPositionUpdater updater) {
		for (int i= fPositionUpdaters.size() - 1; i >= 0; i--) {
			if (fPositionUpdaters.get(i) == updater) {
				fPositionUpdaters.remove(i);
				return;
			}
		}
	}

	private long getNextModificationStamp() {
		if (fNextModificationStamp == Long.MAX_VALUE || fNextModificationStamp == IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP)
			fNextModificationStamp= 0;
		else
			fNextModificationStamp= fNextModificationStamp + 1;

		return fNextModificationStamp;
	}

	@Override
	public long getModificationStamp() {
		return fModificationStamp;
	}

	@Override
	public void replace(int pos, int length, String text, long modificationStamp) throws BadLocationException {
		if ((0 > pos) || (0 > length) || (pos + length > getLength()))
			throw new BadLocationException();

		DocumentEvent e= new DocumentEvent(this, pos, length, text);
		fireDocumentAboutToBeChanged(e);

		getStore().replace(pos, length, text);
		getTracker().replace(pos, length, text);

		fModificationStamp= modificationStamp;
		fNextModificationStamp= Math.max(fModificationStamp, fNextModificationStamp);
		e.fModificationStamp= fModificationStamp;

		fireDocumentChanged(e);
	}

	/**
	 * {@inheritDoc}
	 *
	 * @since 3.4
	 */
	@Override
	public boolean isLineInformationRepairNeeded(int offset, int length, String text) throws BadLocationException {
		return false;
	}

	@Override
	public void replace(int pos, int length, String text) throws BadLocationException {
		if (length == 0 && (text == null || text.isEmpty()))
			replace(pos, length, text, getModificationStamp());
		else
			replace(pos, length, text, getNextModificationStamp());
	}

	@Override
	public void set(String text) {
		set(text, getNextModificationStamp());
	}

	@Override
	public void set(String text, long modificationStamp) {
		int length= getStore().getLength();

		DocumentEvent e= new DocumentEvent(this, 0, length, text);
		fireDocumentAboutToBeChanged(e);

		getStore().set(text);
		getTracker().set(text);

		fModificationStamp= modificationStamp;
		fNextModificationStamp= Math.max(fModificationStamp, fNextModificationStamp);
		e.fModificationStamp= fModificationStamp;

		fireDocumentChanged(e);
	}

	/**
	 * Updates all positions of all categories to the change described by the
	 * document event. All registered document updaters are called in the
	 * sequence they have been arranged. Uses a robust iterator.
	 *
	 * @param event the document event describing the change to which to adapt
	 *            the positions
	 */
	protected void updatePositions(DocumentEvent event) {
		for(IPositionUpdater u: fPositionUpdaters) {
			u.update(event);
		}
	}

	/**
	 * {@inheritDoc}
	 *
	 * @deprecated as of 3.0 search is provided by {@link FindReplaceDocumentAdapter}
	 */
	@Deprecated
	@Override
	public int search(int startPosition, String findString, boolean forwardSearch, boolean caseSensitive, boolean wholeWord) throws BadLocationException {
		try {
			IRegion region= getFindReplaceDocumentAdapter().find(startPosition, findString, forwardSearch, caseSensitive, wholeWord, false);
			return region == null ?  -1 : region.getOffset();
		} catch (IllegalStateException ex) {
			return -1;
		} catch (PatternSyntaxException ex) {
			return -1;
		}
	}

	/**
	 * Returns the find/replace adapter for this document.
	 *
	 * @return this document's find/replace document adapter
	 * @since 3.0
	 */
	private FindReplaceDocumentAdapter getFindReplaceDocumentAdapter() {
		if (fFindReplaceDocumentAdapter == null)
			fFindReplaceDocumentAdapter= new FindReplaceDocumentAdapter(this);

		return fFindReplaceDocumentAdapter;
	}

	/**
	 * Flushes all registered post notification changes.
	 *
	 * @since 2.0
	 */
	private void flushPostNotificationChanges() {
		if (fPostNotificationChanges != null)
			fPostNotificationChanges.clear();
	}

	/**
	 * Executes all registered post notification changes. The process is
	 * repeated until no new post notification changes are added.
	 *
	 * @since 2.0
	 */
	private void executePostNotificationChanges() {

		if (fStoppedCount > 0)
			return;

		while (fPostNotificationChanges != null) {
			List<RegisteredReplace> changes= fPostNotificationChanges;
			fPostNotificationChanges= null;

			Iterator<RegisteredReplace> e= changes.iterator();
			while (e.hasNext()) {
				RegisteredReplace replace= e.next();
				replace.fReplace.perform(this, replace.fOwner);
			}
		}
	}

	@Override
	public void acceptPostNotificationReplaces() {
		fAcceptPostNotificationReplaces= true;
	}

	@Override
	public void ignorePostNotificationReplaces() {
		fAcceptPostNotificationReplaces= false;
	}

	@Override
	public void registerPostNotificationReplace(IDocumentListener owner, IDocumentExtension.IReplace replace) {
		if (fAcceptPostNotificationReplaces) {
			if (fPostNotificationChanges == null)
				fPostNotificationChanges= new ArrayList<>(1);
			fPostNotificationChanges.add(new RegisteredReplace(owner, replace));
		}
	}

	@Override
	public void stopPostNotificationProcessing() {
		++ fStoppedCount;
	}

	@Override
	public void resumePostNotificationProcessing() {
		-- fStoppedCount;
		if (fStoppedCount == 0 && fReentranceCount == 0)
			executePostNotificationChanges();
	}

	/**
	 * {@inheritDoc}
	 *
	 * @since 2.0
	 * @deprecated since 3.1. Use
	 *             {@link IDocumentExtension4#startRewriteSession(DocumentRewriteSessionType)}
	 *             instead.
	 */
	@Deprecated
	@Override
	public void startSequentialRewrite(boolean normalized) {
	}

	/**
	 * {@inheritDoc}
	 *
	 * @since 2.0
	 * @deprecated As of 3.1, replaced by {@link IDocumentExtension4#stopRewriteSession(DocumentRewriteSession)}
	 */
	@Deprecated
	@Override
	public void stopSequentialRewrite() {
	}

	@Override
	public void resumeListenerNotification() {
		-- fStoppedListenerNotification;
		if (fStoppedListenerNotification == 0) {
			resumeDocumentListenerNotification();
		}
	}

	@Override
	public void stopListenerNotification() {
		++ fStoppedListenerNotification;
	}

	/**
	 * Resumes the document listener notification by sending out the remembered
	 * partition changed and document event.
	 *
	 * @since 2.1
	 */
	private void resumeDocumentListenerNotification() {
		if (fDeferredDocumentEvent != null) {
			DocumentEvent event= fDeferredDocumentEvent;
			fDeferredDocumentEvent= null;
			doFireDocumentChanged(event);
		}
	}

	/*
	 * @see org.eclipse.jface.text.IDocumentExtension3#computeZeroLengthPartitioning(java.lang.String, int, int)
	 * @since 3.0
	 */
	@Override
	public ITypedRegion[] computePartitioning(String partitioning, int offset, int length, boolean includeZeroLengthPartitions) throws BadLocationException, BadPartitioningException {
		if ((0 > offset) || (0 > length) || (offset + length > getLength()))
			throw new BadLocationException();

		IDocumentPartitioner partitioner= getDocumentPartitioner(partitioning);

		if (partitioner instanceof IDocumentPartitionerExtension2) {
			checkStateOfPartitioner(partitioner, partitioning);
			return ((IDocumentPartitionerExtension2) partitioner).computePartitioning(offset, length, includeZeroLengthPartitions);
		} else if (partitioner != null) {
			checkStateOfPartitioner(partitioner, partitioning);
			return partitioner.computePartitioning(offset, length);
		} else if (DEFAULT_PARTITIONING.equals(partitioning))
			return new TypedRegion[] { new TypedRegion(offset, length, DEFAULT_CONTENT_TYPE) };
		else
			throw new BadPartitioningException();
	}

	/*
	 * @see org.eclipse.jface.text.IDocumentExtension3#getZeroLengthContentType(java.lang.String, int)
	 * @since 3.0
	 */
	@Override
	public String getContentType(String partitioning, int offset, boolean preferOpenPartitions) throws BadLocationException, BadPartitioningException {
		if ((0 > offset) || (offset > getLength()))
			throw new BadLocationException();

		IDocumentPartitioner partitioner= getDocumentPartitioner(partitioning);

		if (partitioner instanceof IDocumentPartitionerExtension2) {
			checkStateOfPartitioner(partitioner, partitioning);
			return ((IDocumentPartitionerExtension2) partitioner).getContentType(offset, preferOpenPartitions);
		} else if (partitioner != null) {
			checkStateOfPartitioner(partitioner, partitioning);
			return partitioner.getContentType(offset);
		} else if (DEFAULT_PARTITIONING.equals(partitioning))
			return DEFAULT_CONTENT_TYPE;
		else
			throw new BadPartitioningException();
	}

	@Override
	public IDocumentPartitioner getDocumentPartitioner(String partitioning)  {
		return fDocumentPartitioners != null ? fDocumentPartitioners.get(partitioning) : null;
	}

	@Override
	public String[] getLegalContentTypes(String partitioning) throws BadPartitioningException {
		IDocumentPartitioner partitioner= getDocumentPartitioner(partitioning);
		if (partitioner != null)
			return partitioner.getLegalContentTypes();
		if (DEFAULT_PARTITIONING.equals(partitioning))
			return new String[] { DEFAULT_CONTENT_TYPE };
		throw new BadPartitioningException();
	}

	/*
	 * @see org.eclipse.jface.text.IDocumentExtension3#getZeroLengthPartition(java.lang.String, int)
	 * @since 3.0
	 */
	@Override
	public ITypedRegion getPartition(String partitioning, int offset, boolean preferOpenPartitions) throws BadLocationException, BadPartitioningException {
		if ((0 > offset) || (offset > getLength()))
			throw new BadLocationException();

		IDocumentPartitioner partitioner= getDocumentPartitioner(partitioning);

		if (partitioner instanceof IDocumentPartitionerExtension2) {
			checkStateOfPartitioner(partitioner, partitioning);
			return ((IDocumentPartitionerExtension2) partitioner).getPartition(offset, preferOpenPartitions);
		} else if (partitioner != null) {
			checkStateOfPartitioner(partitioner, partitioning);
			return partitioner.getPartition(offset);
		} else if (DEFAULT_PARTITIONING.equals(partitioning))
			return new TypedRegion(0, getLength(), DEFAULT_CONTENT_TYPE);
		else
			throw new BadPartitioningException();
	}

	@Override
	public String[] getPartitionings() {
		if (fDocumentPartitioners == null)
			return new String[0];
		String[] partitionings= new String[fDocumentPartitioners.size()];
		fDocumentPartitioners.keySet().toArray(partitionings);
		return partitionings;
	}

	@Override
	public void setDocumentPartitioner(String partitioning, IDocumentPartitioner partitioner) {
		if (partitioner == null) {
			if (fDocumentPartitioners != null) {
				fDocumentPartitioners.remove(partitioning);
				if (fDocumentPartitioners.isEmpty())
					fDocumentPartitioners= null;
			}
		} else {
			if (fDocumentPartitioners == null)
				fDocumentPartitioners= new HashMap<>();
			fDocumentPartitioners.put(partitioning, partitioner);
		}
		DocumentPartitioningChangedEvent event= new DocumentPartitioningChangedEvent(this);
		event.setPartitionChange(partitioning, 0, getLength());
		fireDocumentPartitioningChanged(event);
	}

	@Override
	public void repairLineInformation() {
		getTracker().set(get());
	}

	/**
	 * Fires the given event to all registered rewrite session listeners. Uses robust iterators.
	 *
	 * @param event the event to be fired
	 * @since 3.1
	 */
	protected void fireRewriteSessionChanged(DocumentRewriteSessionEvent event) {
		if (!fDocumentRewriteSessionListeners.isEmpty()) {
			List<IDocumentRewriteSessionListener> list= new ArrayList<>(fDocumentRewriteSessionListeners);
			Iterator<IDocumentRewriteSessionListener> e= list.iterator();
			while (e.hasNext()) {
				try {
					IDocumentRewriteSessionListener l= e.next();
					l.documentRewriteSessionChanged(event);
				} catch (Exception ex) {
					log(ex);
				}
			}
		}
	}

	@Override
	public final DocumentRewriteSession getActiveRewriteSession() {
		return fDocumentRewriteSession;
	}

	@Override
	public DocumentRewriteSession startRewriteSession(DocumentRewriteSessionType sessionType) {

		if (getActiveRewriteSession() != null)
			throw new IllegalStateException();


		fDocumentRewriteSession= new DocumentRewriteSession(sessionType);
		if (DEBUG)
			System.out.println("AbstractDocument: Starting rewrite session: " + fDocumentRewriteSession); //$NON-NLS-1$

		fireRewriteSessionChanged(new DocumentRewriteSessionEvent(this, fDocumentRewriteSession, DocumentRewriteSessionEvent.SESSION_START));

		startRewriteSessionOnPartitioners(fDocumentRewriteSession);

		ILineTracker tracker= getTracker();
		if (tracker instanceof ILineTrackerExtension) {
			ILineTrackerExtension extension= (ILineTrackerExtension) tracker;
			extension.startRewriteSession(fDocumentRewriteSession);
		}

		if (DocumentRewriteSessionType.SEQUENTIAL == sessionType)
			startSequentialRewrite(false);
		else if (DocumentRewriteSessionType.STRICTLY_SEQUENTIAL == sessionType)
			startSequentialRewrite(true);

		return fDocumentRewriteSession;
	}

	/**
	 * Starts the given rewrite session.
	 *
	 * @param session the rewrite session
	 * @since 3.1
	 */
	protected final void startRewriteSessionOnPartitioners(DocumentRewriteSession session) {
		if (fDocumentPartitioners != null) {
			Iterator<IDocumentPartitioner> e= fDocumentPartitioners.values().iterator();
			while (e.hasNext()) {
				Object partitioner= e.next();
				if (partitioner instanceof IDocumentPartitionerExtension3) {
					IDocumentPartitionerExtension3 extension= (IDocumentPartitionerExtension3) partitioner;
					extension.startRewriteSession(session);
				}
			}
		}
	}

	@Override
	public void stopRewriteSession(DocumentRewriteSession session) {
		if (fDocumentRewriteSession != null && fDocumentRewriteSession == session) {

			if (DEBUG)
				System.out.println("AbstractDocument: Stopping rewrite session: " + session); //$NON-NLS-1$

			DocumentRewriteSessionType sessionType= session.getSessionType();
			if (DocumentRewriteSessionType.SEQUENTIAL == sessionType || DocumentRewriteSessionType.STRICTLY_SEQUENTIAL == sessionType)
				stopSequentialRewrite();

			ILineTracker tracker= getTracker();
			if (tracker instanceof ILineTrackerExtension) {
				ILineTrackerExtension extension= (ILineTrackerExtension) tracker;
				extension.stopRewriteSession(session, get());
			}

			stopRewriteSessionOnPartitioners(fDocumentRewriteSession);

			fDocumentRewriteSession= null;
			fireRewriteSessionChanged(new DocumentRewriteSessionEvent(this, session, DocumentRewriteSessionEvent.SESSION_STOP));
		}
	}

	/**
	 * Stops the given rewrite session.
	 *
	 * @param session the rewrite session
	 * @since 3.1
	 */
	protected final void stopRewriteSessionOnPartitioners(DocumentRewriteSession session) {
		if (fDocumentPartitioners != null) {
			DocumentPartitioningChangedEvent event= new DocumentPartitioningChangedEvent(this);
			for (Entry<String, IDocumentPartitioner> entry : fDocumentPartitioners.entrySet()) {
				String partitioning = entry.getKey();
				IDocumentPartitioner partitioner= entry.getValue();
				if (partitioner instanceof IDocumentPartitionerExtension3) {
					IDocumentPartitionerExtension3 extension= (IDocumentPartitionerExtension3) partitioner;
					extension.stopRewriteSession(session);
					event.setPartitionChange(partitioning, 0, getLength());
				}
			}
			if (!event.isEmpty())
				fireDocumentPartitioningChanged(event);
		}
	}

	@Override
	public void addDocumentRewriteSessionListener(IDocumentRewriteSessionListener listener) {
		Assert.isNotNull(listener);
		if (! fDocumentRewriteSessionListeners.contains(listener))
			fDocumentRewriteSessionListeners.add(listener);
	}

	@Override
	public void removeDocumentRewriteSessionListener(IDocumentRewriteSessionListener listener) {
		Assert.isNotNull(listener);
		fDocumentRewriteSessionListeners.remove(listener);
	}

	/**
	 * Checks the state for the given partitioner and stops the
	 * active rewrite session.
	 *
	 * @param partitioner the document partitioner to be checked
	 * @param partitioning the document partitioning the partitioner is registered for
	 * @since 3.1
	 */
	protected final void checkStateOfPartitioner(IDocumentPartitioner partitioner, String partitioning) {
		if (!(partitioner instanceof IDocumentPartitionerExtension3))
			return;

		IDocumentPartitionerExtension3 extension= (IDocumentPartitionerExtension3) partitioner;
		DocumentRewriteSession session= extension.getActiveRewriteSession();
		if (session != null) {
			extension.stopRewriteSession(session);

			if (DEBUG)
				System.out.println("AbstractDocument: Flushing rewrite session for partition type: " + partitioning); //$NON-NLS-1$

			DocumentPartitioningChangedEvent event= new DocumentPartitioningChangedEvent(this);
			event.setPartitionChange(partitioning, 0, getLength());
			fireDocumentPartitioningChanged(event);
		}
	}

	/**
	 * Returns all positions of the given category that are inside the given region.
	 *
	 * @param category the position category
	 * @param offset the start position of the region, must be &gt;= 0
	 * @param length the length of the region, must be &gt;= 0
	 * @param canStartBefore if <code>true</code> then positions are included
	 *            which start before the region if they end at or after the regions start
	 * @param canEndAfter if <code>true</code> then positions are included
	 *            which end after the region if they start at or before the regions end
	 * @return all positions inside the region of the given category
	 * @throws BadPositionCategoryException if category is undefined in this document
	 * @since 3.4
	 */
	public Position[] getPositions(String category, int offset, int length, boolean canStartBefore, boolean canEndAfter) throws BadPositionCategoryException {
		if (canStartBefore && canEndAfter || (!canStartBefore && !canEndAfter)) {
			List<Position> documentPositions;
			if (canStartBefore && canEndAfter) {
				if (offset < getLength() / 2) {
					documentPositions= getStartingPositions(category, 0, offset + length);
				} else {
					documentPositions= getEndingPositions(category, offset, getLength() - offset + 1);
				}
			} else {
				documentPositions= getStartingPositions(category, offset, length);
			}

			ArrayList<Position> list= new ArrayList<>(documentPositions.size());

			Position region= new Position(offset, length);

			for (Position position : documentPositions) {
				if (isWithinRegion(region, position, canStartBefore, canEndAfter)) {
					list.add(position);
				}
			}

			Position[] positions= new Position[list.size()];
			list.toArray(positions);
			return positions;
		} else if (canStartBefore) {
			List<Position> list= getEndingPositions(category, offset, length);
			Position[] positions= new Position[list.size()];
			list.toArray(positions);
			return positions;
		} else {
			Assert.isLegal(canEndAfter && !canStartBefore);

			List<Position> list= getStartingPositions(category, offset, length);
			Position[] positions= new Position[list.size()];
			list.toArray(positions);
			return positions;
		}
	}

	/*
	 * @since 3.4
	 */
	private boolean isWithinRegion(Position region, Position position, boolean canStartBefore, boolean canEndAfter) {
		if (canStartBefore && canEndAfter) {
			return region.overlapsWith(position.getOffset(), position.getLength());
		} else if (canStartBefore) {
			return region.includes(position.getOffset() + position.getLength() - 1);
		} else if (canEndAfter) {
			return region.includes(position.getOffset());
		} else {
			int start= position.getOffset();
			return region.includes(start) && region.includes(start + position.getLength() - 1);
		}
	}

	/**
	 * A list of positions in the given category with an offset inside the given
	 * region. The order of the positions is arbitrary.
	 *
	 * @param category the position category
	 * @param offset the offset of the region
	 * @param length the length of the region
	 * @return a list of the positions in the region
	 * @throws BadPositionCategoryException if category is undefined in this document
	 * @since 3.4
	 */
	private List<Position> getStartingPositions(String category, int offset, int length) throws BadPositionCategoryException {
		List<Position> positions= fPositions.get(category);
		if (positions == null)
			throw new BadPositionCategoryException();

		int indexStart= computeIndexInPositionList(positions, offset, true);
		int indexEnd= computeIndexInPositionList(positions, offset + length, true);

		return positions.subList(indexStart, indexEnd);
	}

	/**
	 * A list of positions in the given category with an end position inside
	 * the given region. The order of the positions is arbitrary.
	 *
	 * @param category the position category
	 * @param offset the offset of the region
	 * @param length the length of the region
	 * @return a list of the positions in the region
	 * @throws BadPositionCategoryException if category is undefined in this document
	 * @since 3.4
	 */
	private List<Position> getEndingPositions(String category, int offset, int length) throws BadPositionCategoryException {
		List<Position> positions= fEndPositions.get(category);
		if (positions == null)
			throw new BadPositionCategoryException();

		int indexStart= computeIndexInPositionList(positions, offset, false);
		int indexEnd= computeIndexInPositionList(positions, offset + length, false);

		return positions.subList(indexStart, indexEnd);
	}

	/**
	 * Logs the given exception by reusing the code in {@link SafeRunner}.
	 *
	 * @param ex the exception
	 * @since 3.6
	 */
	private static void log(final Exception ex) {
		SafeRunner.run(new ISafeRunnable() {
			@Override
			public void run() throws Exception {
				throw ex;
			}

			@Override
			public void handleException(Throwable exception) {
				// NOTE: Logging is done by SafeRunner
			}
		});
	}

}
