/*******************************************************************************
 * 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.IDocument;
import org.eclipse.jface.text.IDocumentPartitioner;
import org.eclipse.jface.text.IDocumentPartitionerExtension;
import org.eclipse.jface.text.IDocumentPartitionerExtension2;
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 syntax driven document partitioner.
 * It uses a rule based scanner to scan the document and to determine
 * the document's partitioning. The tokens returned by the rules the
 * scanner is configured with 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 IRule
 * @see RuleBasedScanner
 *
 * @deprecated use <code>FastPartitioner</code> instead
 */
@Deprecated
public class RuleBasedPartitioner implements IDocumentPartitioner, IDocumentPartitionerExtension, IDocumentPartitionerExtension2 {

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


	/** The partitioner's scanner */
	protected RuleBasedScanner 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 for managing partitioning information.
	 * @since 3.0
	 */
	private String fPositionCategory;


	/**
	 * 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 RuleBasedPartitioner(RuleBasedScanner 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 };
	}

	/*
	 * @see IDocumentPartitioner#connect
	 */
	@Override
	public void connect(IDocument document) {
		Assert.isNotNull(document);
		Assert.isTrue(!document.containsPositionCategory(fPositionCategory));

		fDocument= document;
		fDocument.addPositionCategory(fPositionCategory);

		initialize();
	}

	/**
	 * Performs the initial partitioning of the partitioner's document.
	 */
	protected void initialize() {

		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
		}
	}

	/*
	 * @see IDocumentPartitioner#disconnect
	 */
	@Override
	public void disconnect() {

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

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

	/*
	 * @see IDocumentPartitioner#documentAboutToBeChanged
	 */
	@Override
	public void documentAboutToBeChanged(DocumentEvent e) {

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

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

	/*
	 * @see IDocumentPartitioner#documentChanged
	 */
	@Override
	public boolean documentChanged(DocumentEvent e) {
		IRegion region= documentChanged2(e);
		return (region != null);
	}

	/**
	 * 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) {

		try {

			IDocument d= e.getDocument();
			Position[] category= d.getPositions(fPositionCategory);
			int first= 0;
			int reparseStart= 0;
			int originalSize= category.length;

			if (originalSize > 0) {

				/*
				 * determine character position at which the scanner starts:
				 * first position behind the last non-default partition the actual position is not involved with
				 */

				first= d.computeIndexInCategory(fPositionCategory, e.getOffset());

				Position p= null;
				do {
					--first;
					if (first < 0)
						break;

					p= category[first];

				} while (p.overlapsWith(e.getOffset(), e.getLength()) ||
							(e.getOffset() == fPreviousDocumentLength &&
							 (p.getOffset() + p.getLength() == fPreviousDocumentLength)));

				fPositionUpdater.update(e);
				for (Position element : category) {
					p= element;
					if (p.isDeleted) {
						rememberDeletedOffset(e.getOffset());
						break;
					}
				}
				category= d.getPositions(fPositionCategory);

				if (first >= 0) {
					p= category[first];
					reparseStart= p.getOffset() + p.getLength();
				}

				++first;
			}

			fScanner.setRange(d, reparseStart, d.getLength() - reparseStart);

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

			while (!token.isEOF()) {


				String 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 we are done
				if (d.containsPosition(fPositionCategory, start, length))
					return createRegion();

				// 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 delete
				++ lastScannedPosition;
			}
			first= d.computeIndexInCategory(fPositionCategory, lastScannedPosition);

			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;
	}


	/*
	 * @see IDocumentPartitioner#getContentType
	 */
	@Override
	public String getContentType(int offset) {

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

		return IDocument.DEFAULT_CONTENT_TYPE;
	}

	/*
	 * @see IDocumentPartitioner#getPartition
	 */
	@Override
	public ITypedRegion getPartition(int offset) {

		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);
	}

	/*
	 * @see IDocumentPartitioner#computePartitioning
	 */
	@Override
	public ITypedRegion[] computePartitioning(int offset, int length) {
		return computePartitioning(offset, length, false);
	}

	/*
	 * @see IDocumentPartitioner#getLegalContentTypes
	 */
	@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) {
		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= null;

			for (Position element : category) {

				current= (TypedPosition) element;

				gapOffset= (previous != null) ? previous.getOffset() + previous.getLength() : 0;
				gap= new Position(gapOffset, current.getOffset() - gapOffset);
				if ((includeZeroLengthPartitions || 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= new Position(gapOffset, fDocument.getLength() - gapOffset);
				if ((includeZeroLengthPartitions || gap.getLength() > 0) && ((includeZeroLengthPartitions && offset + length == gapOffset && gap.length == 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;
	}
}
