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


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

import org.eclipse.core.runtime.Assert;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPositionCategoryException;
import org.eclipse.jface.text.DefaultPositionUpdater;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.DocumentRewriteSession;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentPartitioner;
import org.eclipse.jface.text.IDocumentPartitionerExtension;
import org.eclipse.jface.text.IDocumentPartitionerExtension2;
import org.eclipse.jface.text.IDocumentPartitionerExtension3;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.text.TypedPosition;
import org.eclipse.jface.text.TypedRegion;



/**
 * A standard implementation of a document partitioner. It uses a partition
 * token scanner to scan the document and to determine the document's
 * partitioning. The tokens returned by the scanner are supposed to return the
 * partition type as their data. The partitioner remembers the document's
 * partitions in the document itself rather than maintaining its own data
 * structure.
 *
 * @see IPartitionTokenScanner
 * @since 2.0
 * @deprecated As of 3.1, replaced by {@link org.eclipse.jface.text.rules.FastPartitioner} instead
 */
@Deprecated
public class DefaultPartitioner implements IDocumentPartitioner, IDocumentPartitionerExtension, IDocumentPartitionerExtension2, IDocumentPartitionerExtension3 {

	/**
	 * The position category this partitioner uses to store the document's partitioning information.
	 * @deprecated As of 3.0, use <code>getManagingPositionCategories()</code> instead.
	 */
	@Deprecated
	public final static String CONTENT_TYPES_CATEGORY= "__content_types_category"; //$NON-NLS-1$


	/** The partitioner's scanner */
	protected IPartitionTokenScanner fScanner;
	/** The legal content types of this partitioner */
	protected String[] fLegalContentTypes;
	/** The partitioner's document */
	protected IDocument fDocument;
	/** The document length before a document change occurred */
	protected int fPreviousDocumentLength;
	/** The position updater used to for the default updating of partitions */
	protected DefaultPositionUpdater fPositionUpdater;
	/** The offset at which the first changed partition starts */
	protected int fStartOffset;
	/** The offset at which the last changed partition ends */
	protected int fEndOffset;
	/**The offset at which a partition has been deleted */
	protected int fDeleteOffset;
	/**
	 * The position category this partitioner uses to store the document's partitioning information.
	 * @since 3.0
	 */
	private String fPositionCategory;
	/**
	 * The active document rewrite session.
	 * @since 3.1
	 */
	private DocumentRewriteSession fActiveRewriteSession;
	/**
	 * Flag indicating whether this partitioner has been initialized.
	 * @since 3.1
	 */
	private boolean fIsInitialized= false;

	/**
	 * Creates a new partitioner that uses the given scanner and may return
	 * partitions of the given legal content types.
	 *
	 * @param scanner the scanner this partitioner is supposed to use
	 * @param legalContentTypes the legal content types of this partitioner
	 */
	public DefaultPartitioner(IPartitionTokenScanner scanner, String[] legalContentTypes) {
		fScanner= scanner;
		fLegalContentTypes= TextUtilities.copy(legalContentTypes);
		fPositionCategory= CONTENT_TYPES_CATEGORY + hashCode();
		fPositionUpdater= new DefaultPositionUpdater(fPositionCategory);
	}

	@Override
	public String[] getManagingPositionCategories() {
		return new String[] { fPositionCategory };
	}

	@Override
	public void connect(IDocument document) {
		connect(document, false);
	}

	@Override
	public void connect(IDocument document, boolean delayInitialization) {
		Assert.isNotNull(document);
		Assert.isTrue(!document.containsPositionCategory(fPositionCategory));

		fDocument= document;
		fDocument.addPositionCategory(fPositionCategory);

		fIsInitialized= false;
		if (!delayInitialization)
			checkInitialization();
	}

	/*
	 * @since 3.1
	 */
	protected final void checkInitialization() {
		if (!fIsInitialized)
			initialize();
	}

	/**
	 * Performs the initial partitioning of the partitioner's document.
	 */
	protected void initialize() {
		fIsInitialized= true;
		fScanner.setRange(fDocument, 0, fDocument.getLength());

		try {
			IToken token= fScanner.nextToken();
			while (!token.isEOF()) {

				String contentType= getTokenContentType(token);

				if (isSupportedContentType(contentType)) {
					TypedPosition p= new TypedPosition(fScanner.getTokenOffset(), fScanner.getTokenLength(), contentType);
					fDocument.addPosition(fPositionCategory, p);
				}

				token= fScanner.nextToken();
			}
		} catch (BadLocationException x) {
			// cannot happen as offsets come from scanner
		} catch (BadPositionCategoryException x) {
			// cannot happen if document has been connected before
		}
	}

	@Override
	public void disconnect() {

		Assert.isTrue(fDocument.containsPositionCategory(fPositionCategory));

		try {
			fDocument.removePositionCategory(fPositionCategory);
		} catch (BadPositionCategoryException x) {
			// can not happen because of Assert
		}
	}

	@Override
	public void documentAboutToBeChanged(DocumentEvent e) {
		if (fIsInitialized) {

			Assert.isTrue(e.getDocument() == fDocument);

			fPreviousDocumentLength= e.getDocument().getLength();
			fStartOffset= -1;
			fEndOffset= -1;
			fDeleteOffset= -1;
		}
	}

	@Override
	public boolean documentChanged(DocumentEvent e) {
		if (fIsInitialized) {
			IRegion region= documentChanged2(e);
			return (region != null);
		}
		return false;
	}

	/**
	 * Helper method for tracking the minimal region containing all partition changes.
	 * If <code>offset</code> is smaller than the remembered offset, <code>offset</code>
	 * will from now on be remembered. If <code>offset  + length</code> is greater than
	 * the remembered end offset, it will be remembered from now on.
	 *
	 * @param offset the offset
	 * @param length the length
	 */
	private void rememberRegion(int offset, int length) {
		// remember start offset
		if (fStartOffset == -1)
			fStartOffset= offset;
		else if (offset < fStartOffset)
			fStartOffset= offset;

		// remember end offset
		int endOffset= offset + length;
		if (fEndOffset == -1)
			fEndOffset= endOffset;
		else if (endOffset > fEndOffset)
			fEndOffset= endOffset;
	}

	/**
	 * Remembers the given offset as the deletion offset.
	 *
	 * @param offset the offset
	 */
	private void rememberDeletedOffset(int offset) {
		fDeleteOffset= offset;
	}

	/**
	 * Creates the minimal region containing all partition changes using the
	 * remembered offset, end offset, and deletion offset.
	 *
	 * @return the minimal region containing all the partition changes
	 */
	private IRegion createRegion() {
		if (fDeleteOffset == -1) {
			if (fStartOffset == -1 || fEndOffset == -1)
				return null;
			return new Region(fStartOffset, fEndOffset - fStartOffset);
		} else if (fStartOffset == -1 || fEndOffset == -1) {
			return new Region(fDeleteOffset, 0);
		} else {
			int offset= Math.min(fDeleteOffset, fStartOffset);
			int endOffset= Math.max(fDeleteOffset, fEndOffset);
			return new Region(offset, endOffset - offset);
		}
	}

	@Override
	public IRegion documentChanged2(DocumentEvent e) {

		if (!fIsInitialized)
			return null;

		try {

			IDocument d= e.getDocument();
			Position[] category= d.getPositions(fPositionCategory);
			IRegion line= d.getLineInformationOfOffset(e.getOffset());
			int reparseStart= line.getOffset();
			int partitionStart= -1;
			String contentType= null;
			int newLength= e.getText() == null ? 0 : e.getText().length();

			int first= d.computeIndexInCategory(fPositionCategory, reparseStart);
			if (first > 0)	{
				TypedPosition partition= (TypedPosition) category[first - 1];
				if (partition.includes(reparseStart)) {
					partitionStart= partition.getOffset();
					contentType= partition.getType();
					if (e.getOffset() == partition.getOffset() + partition.getLength())
						reparseStart= partitionStart;
					-- first;
				} else if (reparseStart == e.getOffset() && reparseStart == partition.getOffset() + partition.getLength()) {
					partitionStart= partition.getOffset();
					contentType= partition.getType();
					reparseStart= partitionStart;
					-- first;
				} else {
					partitionStart= partition.getOffset() + partition.getLength();
					contentType= IDocument.DEFAULT_CONTENT_TYPE;
				}
			}

			fPositionUpdater.update(e);
			for (int i= first; i < category.length; i++) {
				Position p= category[i];
				if (p.isDeleted) {
					rememberDeletedOffset(e.getOffset());
					break;
				}
			}
			category= d.getPositions(fPositionCategory);

			fScanner.setPartialRange(d, reparseStart, d.getLength() - reparseStart, contentType, partitionStart);

			int lastScannedPosition= reparseStart;
			IToken token= fScanner.nextToken();

			while (!token.isEOF()) {

				contentType= getTokenContentType(token);

				if (!isSupportedContentType(contentType)) {
					token= fScanner.nextToken();
					continue;
				}

				int start= fScanner.getTokenOffset();
				int length= fScanner.getTokenLength();

				lastScannedPosition= start + length - 1;

				// remove all affected positions
				while (first < category.length) {
					TypedPosition p= (TypedPosition) category[first];
					if (lastScannedPosition >= p.offset + p.length ||
							(p.overlapsWith(start, length) &&
							 	(!d.containsPosition(fPositionCategory, start, length) ||
							 	 !contentType.equals(p.getType())))) {

						rememberRegion(p.offset, p.length);
						d.removePosition(fPositionCategory, p);
						++ first;

					} else
						break;
				}

				// if position already exists and we have scanned at least the
				// area covered by the event, we are done
				if (d.containsPosition(fPositionCategory, start, length)) {
					if (lastScannedPosition >= e.getOffset() + newLength)
						return createRegion();
					++ first;
				} else {
					// insert the new type position
					try {
						d.addPosition(fPositionCategory, new TypedPosition(start, length, contentType));
						rememberRegion(start, length);
					} catch (BadPositionCategoryException x) {
					} catch (BadLocationException x) {
					}
				}

				token= fScanner.nextToken();
			}


			// remove all positions behind lastScannedPosition since there aren't any further types
			if (lastScannedPosition != reparseStart) {
				// if this condition is not met, nothing has been scanned because of a deletion
				++ lastScannedPosition;
			}
			first= d.computeIndexInCategory(fPositionCategory, lastScannedPosition);
			category= d.getPositions(fPositionCategory);

			TypedPosition p;
			while (first < category.length) {
				p= (TypedPosition) category[first++];
				d.removePosition(fPositionCategory, p);
				rememberRegion(p.offset, p.length);
			}

		} catch (BadPositionCategoryException x) {
			// should never happen on connected documents
		} catch (BadLocationException x) {
		}

		return createRegion();
	}


	/**
	 * Returns the position in the partitoner's position category which is
	 * close to the given offset. This is, the position has either an offset which
	 * is the same as the given offset or an offset which is smaller than the given
	 * offset. This method profits from the knowledge that a partitioning is
	 * a ordered set of disjoint position.
	 *
	 * @param offset the offset for which to search the closest position
	 * @return the closest position in the partitioner's category
	 */
	protected TypedPosition findClosestPosition(int offset) {

		try {

			int index= fDocument.computeIndexInCategory(fPositionCategory, offset);
			Position[] category= fDocument.getPositions(fPositionCategory);

			if (category.length == 0)
				return null;

			if (index < category.length) {
				if (offset == category[index].offset)
					return (TypedPosition) category[index];
			}

			if (index > 0)
				index--;

			return (TypedPosition) category[index];

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

		return null;
	}


	@Override
	public String getContentType(int offset) {
		checkInitialization();

		TypedPosition p= findClosestPosition(offset);
		if (p != null && p.includes(offset))
			return p.getType();

		return IDocument.DEFAULT_CONTENT_TYPE;
	}

	@Override
	public ITypedRegion getPartition(int offset) {
		checkInitialization();

		try {

			Position[] category = fDocument.getPositions(fPositionCategory);

			if (category == null || category.length == 0)
				return new TypedRegion(0, fDocument.getLength(), IDocument.DEFAULT_CONTENT_TYPE);

			int index= fDocument.computeIndexInCategory(fPositionCategory, offset);

			if (index < category.length) {

				TypedPosition next= (TypedPosition) category[index];

				if (offset == next.offset)
					return new TypedRegion(next.getOffset(), next.getLength(), next.getType());

				if (index == 0)
					return new TypedRegion(0, next.offset, IDocument.DEFAULT_CONTENT_TYPE);

				TypedPosition previous= (TypedPosition) category[index - 1];
				if (previous.includes(offset))
					return new TypedRegion(previous.getOffset(), previous.getLength(), previous.getType());

				int endOffset= previous.getOffset() + previous.getLength();
				return new TypedRegion(endOffset, next.getOffset() - endOffset, IDocument.DEFAULT_CONTENT_TYPE);
			}

			TypedPosition previous= (TypedPosition) category[category.length - 1];
			if (previous.includes(offset))
				return new TypedRegion(previous.getOffset(), previous.getLength(), previous.getType());

			int endOffset= previous.getOffset() + previous.getLength();
			return new TypedRegion(endOffset, fDocument.getLength() - endOffset, IDocument.DEFAULT_CONTENT_TYPE);

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

		return new TypedRegion(0, fDocument.getLength(), IDocument.DEFAULT_CONTENT_TYPE);
	}

	@Override
	public ITypedRegion[] computePartitioning(int offset, int length) {
		return computePartitioning(offset, length, false);
	}

	@Override
	public String[] getLegalContentTypes() {
		return TextUtilities.copy(fLegalContentTypes);
	}

	/**
	 * Returns whether the given type is one of the legal content types.
	 *
	 * @param contentType the content type to check
	 * @return <code>true</code> if the content type is a legal content type
	 */
	protected boolean isSupportedContentType(String contentType) {
		if (contentType != null) {
			for (String fLegalContentType : fLegalContentTypes) {
				if (fLegalContentType.equals(contentType))
					return true;
			}
		}

		return false;
	}

	/**
	 * Returns a content type encoded in the given token. If the token's
	 * data is not <code>null</code> and a string it is assumed that
	 * it is the encoded content type.
	 *
	 * @param token the token whose content type is to be determined
	 * @return the token's content type
	 */
	protected String getTokenContentType(IToken token) {
		Object data= token.getData();
		if (data instanceof String)
			return (String) data;
		return null;
	}

	/* zero-length partition support */

	@Override
	public String getContentType(int offset, boolean preferOpenPartitions) {
		return getPartition(offset, preferOpenPartitions).getType();
	}

	@Override
	public ITypedRegion getPartition(int offset, boolean preferOpenPartitions) {
		ITypedRegion region= getPartition(offset);
		if (preferOpenPartitions) {
			if (region.getOffset() == offset && !region.getType().equals(IDocument.DEFAULT_CONTENT_TYPE)) {
				if (offset > 0) {
					region= getPartition(offset - 1);
					if (region.getType().equals(IDocument.DEFAULT_CONTENT_TYPE))
						return region;
				}
				return new TypedRegion(offset, 0, IDocument.DEFAULT_CONTENT_TYPE);
			}
		}
		return region;
	}

	@Override
	public ITypedRegion[] computePartitioning(int offset, int length, boolean includeZeroLengthPartitions) {
		checkInitialization();
		List<TypedRegion> list= new ArrayList<>();

		try {

			int endOffset= offset + length;

			Position[] category= fDocument.getPositions(fPositionCategory);

			TypedPosition previous= null, current= null;
			int start, end, gapOffset;
			Position gap= new Position(0);

			int startIndex= getFirstIndexEndingAfterOffset(category, offset);
			int endIndex= getFirstIndexStartingAfterOffset(category, endOffset);
			for (int i= startIndex; i < endIndex; i++) {

				current= (TypedPosition) category[i];

				gapOffset= (previous != null) ? previous.getOffset() + previous.getLength() : 0;
				gap.setOffset(gapOffset);
				gap.setLength(current.getOffset() - gapOffset);
				if ((includeZeroLengthPartitions && overlapsOrTouches(gap, offset, length)) ||
						(gap.getLength() > 0 && gap.overlapsWith(offset, length))) {
					start= Math.max(offset, gapOffset);
					end= Math.min(endOffset, gap.getOffset() + gap.getLength());
					list.add(new TypedRegion(start, end - start, IDocument.DEFAULT_CONTENT_TYPE));
				}

				if (current.overlapsWith(offset, length)) {
					start= Math.max(offset, current.getOffset());
					end= Math.min(endOffset, current.getOffset() + current.getLength());
					list.add(new TypedRegion(start, end - start, current.getType()));
				}

				previous= current;
			}

			if (previous != null) {
				gapOffset= previous.getOffset() + previous.getLength();
				gap.setOffset(gapOffset);
				gap.setLength(fDocument.getLength() - gapOffset);
				if ((includeZeroLengthPartitions && overlapsOrTouches(gap, offset, length)) ||
						(gap.getLength() > 0 && gap.overlapsWith(offset, length))) {
					start= Math.max(offset, gapOffset);
					end= Math.min(endOffset, fDocument.getLength());
					list.add(new TypedRegion(start, end - start, IDocument.DEFAULT_CONTENT_TYPE));
				}
			}

			if (list.isEmpty())
				list.add(new TypedRegion(offset, length, IDocument.DEFAULT_CONTENT_TYPE));

		} catch (BadPositionCategoryException x) {
		}

		TypedRegion[] result= new TypedRegion[list.size()];
		list.toArray(result);
		return result;
	}

	/**
	 * Returns <code>true</code> if the given ranges overlap with or touch each other.
	 *
	 * @param gap the first range
	 * @param offset the offset of the second range
	 * @param length the length of the second range
	 * @return <code>true</code> if the given ranges overlap with or touch each other
	 * @since 3.0
	 */
	private boolean overlapsOrTouches(Position gap, int offset, int length) {
		return gap.getOffset() <= offset + length && offset <= gap.getOffset() + gap.getLength();
	}

	/**
	 * Returns the index of the first position which ends after the given offset.
	 *
	 * @param positions the positions in linear order
	 * @param offset the offset
	 * @return the index of the first position which ends after the offset
	 *
	 * @since 3.0
	 */
	private int getFirstIndexEndingAfterOffset(Position[] positions, int offset) {
		int i= -1, j= positions.length;
		while (j - i > 1) {
			int k= (i + j) >> 1;
			Position p= positions[k];
			if (p.getOffset() + p.getLength() > offset)
				j= k;
			else
				i= k;
		}
		return j;
	}

	/**
	 * Returns the index of the first position which starts at or after the given offset.
	 *
	 * @param positions the positions in linear order
	 * @param offset the offset
	 * @return the index of the first position which starts after the offset
	 *
	 * @since 3.0
	 */
	private int getFirstIndexStartingAfterOffset(Position[] positions, int offset) {
		int i= -1, j= positions.length;
		while (j - i > 1) {
			int k= (i + j) >> 1;
			Position p= positions[k];
			if (p.getOffset() >= offset)
				j= k;
			else
				i= k;
		}
		return j;
	}

	@Override
	public void startRewriteSession(DocumentRewriteSession session) throws IllegalStateException {
		if (fActiveRewriteSession != null)
			throw new IllegalStateException();
		fActiveRewriteSession= session;
	}

	@Override
	public void stopRewriteSession(DocumentRewriteSession session) {
		if (fActiveRewriteSession == session)
			flushRewriteSession();
	}

	@Override
	public DocumentRewriteSession getActiveRewriteSession() {
		return fActiveRewriteSession;
	}

	/**
	 * Flushes the active rewrite session.
	 *
	 * @since 3.1
	 */
	protected final void flushRewriteSession() {
		fActiveRewriteSession= null;

		// remove all position belonging to the partitioner position category
		try {
			fDocument.removePositionCategory(fPositionCategory);
		} catch (BadPositionCategoryException x) {
		}
		fDocument.addPositionCategory(fPositionCategory);

		fIsInitialized= false;
	}
}
