/*******************************************************************************
 * Copyright (c) 2000, 2010 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
 *     Anton Leherbauer <anton.leherbauer@windriver.com> - [projection] "Backspace" key deleting something else - http://bugs.eclipse.org/301023
 *******************************************************************************/
package org.eclipse.jface.text.projection;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.jface.text.AbstractDocument;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPositionCategoryException;
import org.eclipse.jface.text.DefaultLineTracker;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentExtension;
import org.eclipse.jface.text.IDocumentInformationMapping;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.ILineTracker;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextStore;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.TextUtilities;


/**
 * A <code>ProjectionDocument</code> represents a projection of its master
 * document. The contents of a projection document is a sequence of fragments of
 * the master document, i.e. the projection document can be thought as being
 * constructed from the master document by not copying the whole master document
 * but omitting several ranges of the master document.
 * <p>
 * The projection document indirectly utilizes its master document as
 * <code>ITextStore</code> by means of a <code>ProjectionTextStore</code>.
 * <p>
 * The content of a projection document can be changed in two ways. Either by a
 * text replace applied to the master document or the projection document. Or by
 * changing the projection between the master document and the projection
 * document. For the latter the two methods <code>addMasterDocumentRange</code>
 * and <code>removeMasterDocumentRange</code> are provided. For any
 * manipulation, the projection document sends out a
 * {@link org.eclipse.jface.text.projection.ProjectionDocumentEvent} describing
 * the change.
 * <p>
 * Clients are not supposed to directly instantiate this class. In order to
 * obtain a projection document, a
 * {@link org.eclipse.jface.text.projection.ProjectionDocumentManager}should be
 * used. This class is not intended to be subclassed outside of its origin
 * package.</p>
 *
 * @since 3.0
 * @noinstantiate This class is not intended to be instantiated by clients.
 * @noextend This class is not intended to be subclassed by clients.
 */
public class ProjectionDocument extends AbstractDocument {


	/**
	 * Prefix of the name of the position category used to keep track of the master
	 * document's fragments that correspond to the segments of the projection
	 * document.
	 */
	private final static String FRAGMENTS_CATEGORY_PREFIX= "__fragmentsCategory"; //$NON-NLS-1$

	/**
	 * Name of the position category used to keep track of the project
	 * document's segments that correspond to the fragments of the master
	 * document.
	 */
	private final static String SEGMENTS_CATEGORY= "__segmentsCategory"; //$NON-NLS-1$


	/** The master document */
	private IDocument fMasterDocument;
	/** The master document as document extension */
	private IDocumentExtension fMasterDocumentExtension;
	/** The fragments' position category */
	private String fFragmentsCategory;
	/** The segment's position category */
	private String fSegmentsCategory;
	/** The document event issued by the master document */
	private DocumentEvent fMasterEvent;
	/** The document event to be issued by the projection document */
	private ProjectionDocumentEvent fSlaveEvent;
	/** The original document event generated by a direct manipulation of this projection document */
	private DocumentEvent fOriginalEvent;
	/** Indicates whether the projection document initiated a master document update or not */
	private boolean fIsUpdating= false;
	/** Indicated whether the projection document is in auto expand mode nor not */
	private boolean fIsAutoExpanding= false;
	/** The position updater for the segments */
	private SegmentUpdater fSegmentUpdater;
	/** The position updater for the fragments */
	private FragmentUpdater fFragmentsUpdater;
	/** The projection mapping */
	private ProjectionMapping fMapping;

	/**
	 * Creates a projection document for the given master document.
	 *
	 * @param masterDocument the master document
	 */
	public ProjectionDocument(IDocument masterDocument) {
		super();

		fMasterDocument= masterDocument;
		if (fMasterDocument instanceof IDocumentExtension)
			fMasterDocumentExtension= (IDocumentExtension) fMasterDocument;

		fSegmentsCategory= SEGMENTS_CATEGORY;
		fFragmentsCategory= FRAGMENTS_CATEGORY_PREFIX + hashCode();
		fMasterDocument.addPositionCategory(fFragmentsCategory);
		fFragmentsUpdater= new FragmentUpdater(fFragmentsCategory);
		fMasterDocument.addPositionUpdater(fFragmentsUpdater);

		fMapping= new ProjectionMapping(masterDocument, fFragmentsCategory, this, fSegmentsCategory);

		ITextStore s= new ProjectionTextStore(masterDocument, fMapping);
		ILineTracker tracker= new DefaultLineTracker();

		setTextStore(s);
		setLineTracker(tracker);

		completeInitialization();

		initializeProjection();
		tracker.set(s.get(0, s.getLength()));
	}

	/**
	 * Disposes this projection document.
	 */
	public void dispose() {
		fMasterDocument.removePositionUpdater(fFragmentsUpdater);
		try {
			fMasterDocument.removePositionCategory(fFragmentsCategory);
		} catch (BadPositionCategoryException x) {
			// allow multiple dispose calls
		}
	}

	private void internalError() {
		throw new IllegalStateException();
	}

	/**
	 * Returns the fragments of the master documents.
	 *
	 * @return the fragment of the master document
	 */
	protected final Position[] getFragments() {
		try {
			return fMasterDocument.getPositions(fFragmentsCategory);
		} catch (BadPositionCategoryException e) {
			internalError();
		}
		// unreachable
		return null;
	}

	/**
	 * Returns the segments of this projection document.
	 *
	 * @return the segments of this projection document
	 */
	protected final Position[] getSegments() {
		try {
			return getPositions(fSegmentsCategory);
		} catch (BadPositionCategoryException e) {
			internalError();
		}
		// unreachable
		return null;
	}

	/**
	 * Returns the projection mapping used by this document.
	 *
	 * @return the projection mapping used by this document
	 * @deprecated As of 3.4, replaced by {@link #getDocumentInformationMapping()}
	 */
	@Deprecated
	public ProjectionMapping getProjectionMapping(){
		return fMapping;
	}

	/**
	 * Returns the projection mapping used by this document.
	 *
	 * @return the projection mapping used by this document
	 * @since 3.4
	 */
	public IDocumentInformationMapping getDocumentInformationMapping() {
		return fMapping;
	}

	/**
	 * Returns the master document of this projection document.
	 *
	 * @return the master document of this projection document
	 */
	public IDocument getMasterDocument() {
		return fMasterDocument;
	}

	@Override
	public String getDefaultLineDelimiter() {
		return TextUtilities.getDefaultLineDelimiter(fMasterDocument);
	}

	/**
	 * Initializes the projection document from the master document based on
	 * the master's fragments.
	 */
	private void initializeProjection() {

		try {

			addPositionCategory(fSegmentsCategory);
			fSegmentUpdater= new SegmentUpdater(fSegmentsCategory);
			addPositionUpdater(fSegmentUpdater);

			int offset= 0;
			Position[] fragments= getFragments();
			for (Position f : fragments) {
				Fragment fragment = (Fragment) f;
				Segment segment= new Segment(offset, fragment.getLength());
				segment.fragment= fragment;
				addPosition(fSegmentsCategory, segment);
				offset += fragment.length;
			}

		} catch (BadPositionCategoryException x) {
			internalError();
		} catch (BadLocationException x) {
			internalError();
		}
	}

	/**
	 * Creates a segment for the given fragment at the given position inside the list of segments.
	 *
	 * @param fragment the corresponding fragment
	 * @param index the index in the list of segments
	 * @return the created segment
	 * @throws BadLocationException in case the fragment is invalid
	 * @throws BadPositionCategoryException in case the segment category is invalid
	 */
	private Segment createSegmentFor(Fragment fragment, int index) throws BadLocationException, BadPositionCategoryException {

		int offset= 0;
		if (index > 0) {
			Position[] segments= getSegments();
			Segment segment= (Segment) segments[index - 1];
			offset= segment.getOffset() + segment.getLength();
		}

		Segment segment= new Segment(offset, 0);
		segment.fragment= fragment;
		fragment.segment= segment;
		addPosition(fSegmentsCategory, segment);
		return segment;
	}

	/**
	 * Adds the given range of the master document to this projection document.
	 *
	 * @param offsetInMaster offset of the master document range
	 * @param lengthInMaster length of the master document range
	 * @param masterDocumentEvent the master document event that causes this
	 *            projection change or <code>null</code> if none
	 * @throws BadLocationException if the given range is invalid in the master
	 *             document
	 */
	private void internalAddMasterDocumentRange(int offsetInMaster, int lengthInMaster, DocumentEvent masterDocumentEvent) throws BadLocationException {
		if (lengthInMaster == 0)
			return;

		try {

			Position[] fragments= getFragments();
			int index= fMasterDocument.computeIndexInCategory(fFragmentsCategory, offsetInMaster);

			Fragment left= null;
			Fragment right= null;

			if (index < fragments.length) {
				Fragment fragment= (Fragment) fragments[index];
				if (offsetInMaster == fragment.offset)
					if (fragment.length == 0) // the fragment does not overlap - it is a zero-length fragment at the same offset
						left= fragment;
					else
						throw new IllegalArgumentException("overlaps with existing fragment"); //$NON-NLS-1$
				if (offsetInMaster + lengthInMaster == fragment.offset)
					right= fragment;
			}

			if (0 < index && index <= fragments.length) {
				Fragment fragment= (Fragment) fragments[index - 1];
				if (fragment.includes(offsetInMaster))
					throw new IllegalArgumentException("overlaps with existing fragment"); //$NON-NLS-1$
				if (fragment.getOffset() + fragment.getLength() == offsetInMaster)
					left= fragment;
			}

			int offsetInSlave= 0;
			if (index > 0) {
				Fragment fragment= (Fragment) fragments[index - 1];
				Segment segment= fragment.segment;
				offsetInSlave= segment.getOffset() + segment.getLength();
			}

			ProjectionDocumentEvent event= new ProjectionDocumentEvent(this, offsetInSlave, 0, fMasterDocument.get(offsetInMaster, lengthInMaster), offsetInMaster, lengthInMaster, masterDocumentEvent);
			super.fireDocumentAboutToBeChanged(event);

			// check for neighboring fragment
			if (left != null && right != null) {

				int endOffset= right.getOffset() + right.getLength();
				left.setLength(endOffset - left.getOffset());
				left.segment.setLength(left.segment.getLength() + right.segment.getLength());

				removePosition(fSegmentsCategory, right.segment);
				fMasterDocument.removePosition(fFragmentsCategory, right);

			} else if (left != null) {
				int endOffset= offsetInMaster +lengthInMaster;
				left.setLength(endOffset - left.getOffset());
				left.segment.markForStretch();

			} else if (right != null) {
				right.setOffset(right.getOffset() - lengthInMaster);
				right.setLength(right.getLength() + lengthInMaster);
				right.segment.markForStretch();

			} else {
				// create a new segment
				Fragment fragment= new Fragment(offsetInMaster, lengthInMaster);
				fMasterDocument.addPosition(fFragmentsCategory, fragment);
				Segment segment= createSegmentFor(fragment, index);
				segment.markForStretch();
			}

			getTracker().replace(event.getOffset(), event.getLength(), event.getText());
			super.fireDocumentChanged(event);

		} catch (BadPositionCategoryException x) {
			internalError();
		}
	}

	/**
	 * Finds the fragment of the master document that represents the given range.
	 *
	 * @param offsetInMaster the offset of the range in the master document
	 * @param lengthInMaster the length of the range in the master document
	 * @return the fragment representing the given master document range
	 */
	private Fragment findFragment(int offsetInMaster, int lengthInMaster) {
		Position[] fragments= getFragments();
		for (Position fragment : fragments) {
			Fragment f = (Fragment) fragment;
			if (f.getOffset() <= offsetInMaster && offsetInMaster + lengthInMaster <= f.getOffset() + f.getLength())
				return f;
		}
		return null;
	}

	/**
	 * Removes the given range of the master document from this projection
	 * document.
	 *
	 * @param offsetInMaster the offset of the range in the master document
	 * @param lengthInMaster the length of the range in the master document
	 *
	 * @throws BadLocationException if the given range is not valid in the
	 *             master document
	 * @throws IllegalArgumentException if the given range is not projected in
	 *             this projection document or is not completely comprised by
	 *             an existing fragment
	 */
	private void internalRemoveMasterDocumentRange(int offsetInMaster, int lengthInMaster) throws BadLocationException {
		try {

			IRegion imageRegion= fMapping.toExactImageRegion(new Region(offsetInMaster, lengthInMaster));
			if (imageRegion == null)
				throw new IllegalArgumentException();

			Fragment fragment= findFragment(offsetInMaster, lengthInMaster);
			if (fragment == null)
				throw new IllegalArgumentException();

			ProjectionDocumentEvent event= new ProjectionDocumentEvent(this, imageRegion.getOffset(), imageRegion.getLength(), "", offsetInMaster, lengthInMaster); //$NON-NLS-1$
			super.fireDocumentAboutToBeChanged(event);

			if (fragment.getOffset() == offsetInMaster) {
				fragment.setOffset(offsetInMaster + lengthInMaster);
				fragment.setLength(fragment.getLength() - lengthInMaster);
			} else {
				// split fragment into three fragments, let position updater remove it

				// add fragment for the region to be removed
				Fragment newFragment= new Fragment(offsetInMaster, lengthInMaster);
				Segment segment= new Segment(imageRegion.getOffset(), imageRegion.getLength());
				newFragment.segment= segment;
				segment.fragment= newFragment;
				fMasterDocument.addPosition(fFragmentsCategory, newFragment);
				addPosition(fSegmentsCategory, segment);

				// add fragment for the remainder right of the deleted range in the original fragment
				int offset= offsetInMaster + lengthInMaster;
				newFragment= new Fragment(offset, fragment.getOffset() + fragment.getLength() - offset);
				offset= imageRegion.getOffset() + imageRegion.getLength();
				segment= new Segment(offset, fragment.segment.getOffset() + fragment.segment.getLength() - offset);
				newFragment.segment= segment;
				segment.fragment= newFragment;
				fMasterDocument.addPosition(fFragmentsCategory, newFragment);
				addPosition(fSegmentsCategory, segment);

				// adjust length of initial fragment (the left one)
				fragment.setLength(offsetInMaster - fragment.getOffset());
				fragment.segment.setLength(imageRegion.getOffset() - fragment.segment.getOffset());
			}

			getTracker().replace(event.getOffset(), event.getLength(), event.getText());
			super.fireDocumentChanged(event);

		} catch (BadPositionCategoryException x) {
			internalError();
		}
	}

	/**
	 * Returns the sequence of all master document regions which are contained
	 * in the given master document range and which are not yet part of this
	 * projection document.
	 *
	 * @param offsetInMaster the range offset in the master document
	 * @param lengthInMaster the range length in the master document
	 * @return the sequence of regions which are not yet part of the projection
	 *         document
	 * @throws BadLocationException in case the given range is invalid in the
	 *         master document
	 */
	public final IRegion[] computeUnprojectedMasterRegions(int offsetInMaster, int lengthInMaster) throws BadLocationException {

		IRegion[] fragments= null;
		IRegion imageRegion= fMapping.toImageRegion(new Region(offsetInMaster, lengthInMaster));
		if (imageRegion != null)
			fragments= fMapping.toExactOriginRegions(imageRegion);

		if (fragments == null || fragments.length == 0)
			return new IRegion[] { new Region(offsetInMaster, lengthInMaster) };

		List<Region> gaps= new ArrayList<>();

		IRegion region= fragments[0];
		if (offsetInMaster < region.getOffset())
			gaps.add(new Region(offsetInMaster, region.getOffset() - offsetInMaster));

		for (int i= 0; i < fragments.length - 1; i++) {
			IRegion left= fragments[i];
			IRegion right= fragments[i + 1];
			int leftEnd= left.getOffset() + left.getLength();
			if (leftEnd < right.getOffset())
				gaps.add(new Region(leftEnd, right.getOffset() - leftEnd));
		}

		region= fragments[fragments.length - 1];
		int leftEnd= region.getOffset() + region.getLength();
		int rightEnd= offsetInMaster + lengthInMaster;
		if (leftEnd < rightEnd)
			gaps.add(new Region(leftEnd, rightEnd - leftEnd));

		IRegion[] result= new IRegion[gaps.size()];
		gaps.toArray(result);
		return result;
	}

	/**
	 * Returns the first master document region which is contained in the given
	 * master document range and which is not yet part of this projection
	 * document.
	 *
	 * @param offsetInMaster the range offset in the master document
	 * @param lengthInMaster the range length in the master document
	 * @return the first region that is not yet part of the projection document
	 * @throws BadLocationException in case the given range is invalid in the
	 *         master document
	 * @since 3.1
	 */
	private IRegion computeFirstUnprojectedMasterRegion(int offsetInMaster, int lengthInMaster) throws BadLocationException {

		IRegion[] fragments= null;
		IRegion imageRegion= fMapping.toImageRegion(new Region(offsetInMaster, lengthInMaster));
		if (imageRegion != null)
			fragments= fMapping.toExactOriginRegions(imageRegion);

		if (fragments == null || fragments.length == 0)
			return new Region(offsetInMaster, lengthInMaster);

		IRegion region= fragments[0];
		if (offsetInMaster < region.getOffset())
			return new Region(offsetInMaster, region.getOffset() - offsetInMaster);

		for (int i= 0; i < fragments.length - 1; i++) {
			IRegion left= fragments[i];
			IRegion right= fragments[i + 1];
			int leftEnd= left.getOffset() + left.getLength();
			if (leftEnd < right.getOffset())
				return new Region(leftEnd, right.getOffset() - leftEnd);
		}

		region= fragments[fragments.length - 1];
		int leftEnd= region.getOffset() + region.getLength();
		int rightEnd= offsetInMaster + lengthInMaster;
		if (leftEnd < rightEnd)
			return new Region(leftEnd, rightEnd - leftEnd);

		return null;
	}

	/**
	 * Ensures that the given range of the master document is part of this
	 * projection document.
	 *
	 * @param offsetInMaster the offset of the master document range
	 * @param lengthInMaster the length of the master document range
	 * @throws BadLocationException in case the master event is not valid
	 */
	public void addMasterDocumentRange(int offsetInMaster, int lengthInMaster) throws BadLocationException {
		addMasterDocumentRange(offsetInMaster, lengthInMaster, null);
	}

	/**
	 * Ensures that the given range of the master document is part of this
	 * projection document.
	 *
	 * @param offsetInMaster the offset of the master document range
	 * @param lengthInMaster the length of the master document range
	 * @param masterDocumentEvent the master document event which causes this
	 *            projection change, or <code>null</code> if none
	 * @throws BadLocationException in case the master event is not valid
	 */
	private void addMasterDocumentRange(int offsetInMaster, int lengthInMaster, DocumentEvent masterDocumentEvent) throws BadLocationException {
		/*
		 * Calling internalAddMasterDocumentRange may cause other master ranges
		 * to become unfolded, resulting in re-entrant calls to this method. In
		 * order to not add a region twice, we have to compute the next region
		 * to add in every iteration.
		 *
		 * To place an upper bound on the number of iterations, we use the number
		 * of fragments * 2 as the limit.
		 */
		int limit= Math.max(getFragments().length * 2, 20);
		while (true) {
			if (limit-- < 0)
				throw new IllegalArgumentException("safety loop termination"); //$NON-NLS-1$

			IRegion gap= computeFirstUnprojectedMasterRegion(offsetInMaster, lengthInMaster);
			if (gap == null)
				return;

			internalAddMasterDocumentRange(gap.getOffset(), gap.getLength(), masterDocumentEvent);
		}
	}

	/**
	 * Ensures that the given range of the master document is not part of this
	 * projection document.
	 *
	 * @param offsetInMaster the offset of the master document range
	 * @param lengthInMaster the length of the master document range
	 * @throws BadLocationException in case the master event is not valid
	 */
	public void removeMasterDocumentRange(int offsetInMaster, int lengthInMaster) throws BadLocationException {
		IRegion[] fragments= computeProjectedMasterRegions(offsetInMaster, lengthInMaster);
		if (fragments == null || fragments.length == 0)
			return;

		for (IRegion fragment : fragments) {
			internalRemoveMasterDocumentRange(fragment.getOffset(), fragment.getLength());
		}
	}

	/**
	 * Returns the sequence of all master document regions with are contained in the given master document
	 * range and which are part of this projection document. May return <code>null</code> if no such
	 * regions exist.
	 *
	 * @param offsetInMaster the range offset in the master document
	 * @param lengthInMaster the range length in the master document
	 * @return the sequence of regions which are part of the projection document or <code>null</code>
	 * @throws BadLocationException in case the given range is invalid in the master document
	 */
	public final IRegion[] computeProjectedMasterRegions(int offsetInMaster, int lengthInMaster) throws BadLocationException {
		IRegion imageRegion= fMapping.toImageRegion(new Region(offsetInMaster, lengthInMaster));
		return imageRegion != null ? fMapping.toExactOriginRegions(imageRegion) : null;
	}

	/**
	 * Returns whether this projection is being updated.
	 *
	 * @return <code>true</code> if the document is updating
	 */
	protected boolean isUpdating() {
		return fIsUpdating;
	}

	@Override
	public void replace(int offset, int length, String text) throws BadLocationException {
		try {
			fIsUpdating= true;
			if (fMasterDocumentExtension != null)
				fMasterDocumentExtension.stopPostNotificationProcessing();
			super.replace(offset, length, text);
		} finally {
			fIsUpdating= false;
			if (fMasterDocumentExtension != null)
				fMasterDocumentExtension.resumePostNotificationProcessing();
		}
	}

	@Override
	public void set(String text) {
		try {
			fIsUpdating= true;
			if (fMasterDocumentExtension != null)
				fMasterDocumentExtension.stopPostNotificationProcessing();

			super.set(text);

		} finally {
			fIsUpdating= false;
			if (fMasterDocumentExtension != null)
				fMasterDocumentExtension.resumePostNotificationProcessing();
		}
	}

	/**
	 * Transforms a document event of the master document into a projection
	 * document based document event.
	 *
	 * @param masterEvent the master document event
	 * @return the slave document event
	 * @throws BadLocationException in case the master event is not valid
	 */
	private ProjectionDocumentEvent normalize(DocumentEvent masterEvent) throws BadLocationException {
		if (!isUpdating()) {
			IRegion imageRegion= fMapping.toExactImageRegion(new Region(masterEvent.getOffset(), masterEvent.getLength()));
			if (imageRegion != null)
				return new ProjectionDocumentEvent(this, imageRegion.getOffset(), imageRegion.getLength(), masterEvent.getText(), masterEvent);
			return null;
		}

		ProjectionDocumentEvent event= new ProjectionDocumentEvent(this, fOriginalEvent.getOffset(), fOriginalEvent.getLength(), fOriginalEvent.getText(), masterEvent);
		fOriginalEvent= null;
		return event;
	}

	/**
	 * Ensures that when the master event affects this projection document, that the whole region described by the
	 * event is part of this projection document.
	 *
	 * @param masterEvent the master document event
	 * @return <code>true</code> if masterEvent affects this projection document
	 * @throws BadLocationException in case the master event is not valid
	 */
	protected final boolean adaptProjectionToMasterChange(DocumentEvent masterEvent) throws BadLocationException {
		if (!isUpdating() && fFragmentsUpdater.affectsPositions(masterEvent) || fIsAutoExpanding && masterEvent.getLength() > 0) {

			addMasterDocumentRange(masterEvent.getOffset(), masterEvent.getLength(), masterEvent);
			return true;

		} else if (fMapping.getImageLength() == 0 && masterEvent.getLength() == 0) {

			Position[] fragments= getFragments();
			if (fragments.length == 0) {
				// there is no segment in this projection document, thus one must be created
				// need to bypass the usual infrastructure as the new segment/fragment would be of length 0 and thus the segmentation be not well formed
				try {
					Fragment fragment= new Fragment(0, 0);
					fMasterDocument.addPosition(fFragmentsCategory, fragment);
					createSegmentFor(fragment, 0);
				} catch (BadPositionCategoryException x) {
					internalError();
				}
			}
		}

		return isUpdating();
	}

	/**
	 * When called, this projection document is informed about a forthcoming
	 * change of its master document. This projection document checks whether
	 * the master document change affects it and if so informs all document
	 * listeners.
	 *
	 * @param masterEvent the master document event
	 */
	public void masterDocumentAboutToBeChanged(DocumentEvent masterEvent) {
		try {

			boolean assertNotNull= adaptProjectionToMasterChange(masterEvent);
			fSlaveEvent= normalize(masterEvent);
			if (assertNotNull && fSlaveEvent == null)
				internalError();

			fMasterEvent= masterEvent;
			if (fSlaveEvent != null)
				delayedFireDocumentAboutToBeChanged();

		} catch (BadLocationException e) {
			internalError();
		}
	}

	/**
	 * When called, this projection document is informed about a change of its
	 * master document. If this projection document is affected it informs all
	 * of its document listeners.
	 *
	 * @param masterEvent the master document event
	 */
	public void masterDocumentChanged(DocumentEvent masterEvent) {
		if ( !isUpdating() && masterEvent == fMasterEvent) {
			if (fSlaveEvent != null) {
				try {
					getTracker().replace(fSlaveEvent.getOffset(), fSlaveEvent.getLength(), fSlaveEvent.getText());
					fireDocumentChanged(fSlaveEvent);
				} catch (BadLocationException e) {
					internalError();
				}
			} else if (ensureWellFormedSegmentation(masterEvent.getOffset()))
				fMapping.projectionChanged();
		}
	}

	@Override
	protected void fireDocumentAboutToBeChanged(DocumentEvent event) {
		fOriginalEvent= event;
		// delay it until there is a notification from the master document
		// at this point, it is expensive to construct the master document information
	}

	/**
	 * Fires the slave document event as about-to-be-changed event to all registered listeners.
	 */
	private void delayedFireDocumentAboutToBeChanged() {
		super.fireDocumentAboutToBeChanged(fSlaveEvent);
	}

	/**
	 * Ignores the given event and sends the semantically equal slave document event instead.
	 *
	 * @param event the event to be ignored
	 */
	@Override
	protected void fireDocumentChanged(DocumentEvent event) {
		super.fireDocumentChanged(fSlaveEvent);
	}

	@Override
	protected void updateDocumentStructures(DocumentEvent event) {
		super.updateDocumentStructures(event);
		ensureWellFormedSegmentation(computeAnchor(event));
		fMapping.projectionChanged();
	}

	private int computeAnchor(DocumentEvent event) {
		if (event instanceof ProjectionDocumentEvent) {
			ProjectionDocumentEvent slave= (ProjectionDocumentEvent) event;
			Object changeType= slave.getChangeType();
			if (ProjectionDocumentEvent.CONTENT_CHANGE == changeType) {
				DocumentEvent master= slave.getMasterEvent();
				if (master != null)
					return master.getOffset();
			} else if (ProjectionDocumentEvent.PROJECTION_CHANGE == changeType) {
				return slave.getMasterOffset();
			}
		}
		return -1;
	}

	private boolean ensureWellFormedSegmentation(int anchorOffset) {
		boolean changed= false;
		Position[] segments= getSegments();
		for (int i= 0; i < segments.length; i++) {
			Segment segment= (Segment) segments[i];
			if (segment.isDeleted() || (segment.getLength() == 0 && (i < segments.length - 1 || (i > 0 && segments[i - 1].isDeleted())))) {
				try {
					removePosition(fSegmentsCategory, segment);
					fMasterDocument.removePosition(fFragmentsCategory, segment.fragment);
					changed= true;
				} catch (BadPositionCategoryException e) {
					internalError();
				}
			} else if (i < segments.length - 1) {
				Segment next= (Segment) segments[i + 1];
				if (next.isDeleted() || next.getLength() == 0)
					continue;
				Fragment fragment= segment.fragment;
				if (fragment.getOffset() + fragment.getLength() == next.fragment.getOffset()) {
					// join fragments and their corresponding segments
					segment.setLength(segment.getLength() + next.getLength());
					fragment.setLength(fragment.getLength() + next.fragment.getLength());
					next.delete();
				}
			}
		}

		if (changed && anchorOffset != -1) {
			Position[] changedSegments= getSegments();
			if (changedSegments == null || changedSegments.length == 0) {
				Fragment fragment= new Fragment(anchorOffset, 0);
				try {
					fMasterDocument.addPosition(fFragmentsCategory, fragment);
					createSegmentFor(fragment, 0);
				} catch (BadLocationException e) {
					internalError();
				} catch (BadPositionCategoryException e) {
					internalError();
				}
			}
		}

		return changed;
	}

	@Override
	public void registerPostNotificationReplace(IDocumentListener owner, IDocumentExtension.IReplace replace) {
		if (!isUpdating())
			throw new UnsupportedOperationException();
		super.registerPostNotificationReplace(owner, replace);
	}

	/**
	 * Sets the auto expand mode for this document.
	 *
	 * @param autoExpandMode <code>true</code> if auto-expanding
	 */
	public void setAutoExpandMode(boolean autoExpandMode) {
		fIsAutoExpanding= autoExpandMode;
	}

	/**
	 * Replaces all master document ranges with the given master document range.
	 *
	 * @param offsetInMaster the offset in the master document
	 * @param lengthInMaster the length in the master document
	 * @throws BadLocationException if the given range of the master document is not valid
	 */
	public void replaceMasterDocumentRanges(int offsetInMaster, int lengthInMaster) throws BadLocationException {
		try {

			ProjectionDocumentEvent event= new ProjectionDocumentEvent(this, 0, fMapping.getImageLength(), fMasterDocument.get(offsetInMaster, lengthInMaster), offsetInMaster, lengthInMaster);
			super.fireDocumentAboutToBeChanged(event);

			Position[] fragments= getFragments();
			for (Position fragment1 : fragments) {
				Fragment fragment = (Fragment) fragment1;
				fMasterDocument.removePosition(fFragmentsCategory, fragment);
				removePosition(fSegmentsCategory, fragment.segment);
			}

			Fragment fragment= new Fragment(offsetInMaster, lengthInMaster);
			Segment segment= new Segment(0, 0);
			segment.fragment= fragment;
			fragment.segment= segment;
			fMasterDocument.addPosition(fFragmentsCategory, fragment);
			addPosition(fSegmentsCategory, segment);

			getTracker().set(fMasterDocument.get(offsetInMaster, lengthInMaster));
			super.fireDocumentChanged(event);

		} catch (BadPositionCategoryException x) {
			internalError();
		}
	}
}
