/*******************************************************************************
 * Copyright (c) 2001, 2011 IBM Corporation and others.
 * All rights reserved. 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
 *     Jens Lukowski/Innoopract - initial renaming/restructuring
 *     
 *******************************************************************************/
package org.eclipse.wst.sse.core.internal.text.rules;



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

import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentPartitioner;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.wst.sse.core.internal.ltk.parser.IBlockedStructuredDocumentRegion;
import org.eclipse.wst.sse.core.internal.parser.ForeignRegion;
import org.eclipse.wst.sse.core.internal.provisional.events.StructuredDocumentRegionsReplacedEvent;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredTextPartitioner;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionCollection;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionContainer;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList;
import org.eclipse.wst.sse.core.text.IStructuredPartitions;


/**
 * Base Document partitioner for StructuredDocuments. BLOCK_TEXT ITextRegions
 * have a partition type of BLOCK or BLOCK:TAGNAME if a surrounding tagname
 * was recorded.
 * 
 * Subclasses should synchronize access to <code>internalReusedTempInstance</code> using the lock
 * <code>PARTITION_LOCK</code>.
 */
public class StructuredTextPartitioner implements IDocumentPartitioner, IStructuredTextPartitioner {

	static class CachedComputedPartitions {
		int fLength;
		int fOffset;
		ITypedRegion[] fPartitions;
		boolean isInValid;

		CachedComputedPartitions(int offset, int length, ITypedRegion[] partitions) {
			fOffset = offset;
			fLength = length;
			fPartitions = partitions;
			isInValid = true;
		}
	}

	private CachedComputedPartitions cachedPartitions = new CachedComputedPartitions(-1, -1, null);
	protected String[] fSupportedTypes = null;
	protected IStructuredTypedRegion internalReusedTempInstance = new SimpleStructuredTypedRegion(0, 0, IStructuredPartitions.DEFAULT_PARTITION);
	protected IStructuredDocument fStructuredDocument;

	protected final Object PARTITION_LOCK = new Object();

	/**
	 * StructuredTextPartitioner constructor comment.
	 */
	public StructuredTextPartitioner() {
		super();
	}

	/**
	 * Returns the partitioning of the given range of the connected document.
	 * There must be a document connected to this partitioner.
	 * 
	 * Note: this shouldn't be called directly by clients, unless they control
	 * the threading that includes modifications to the document. Otherwise
	 * the document could be modified while partitions are being computed. We
	 * advise that clients use the computePartitions API directly from the
	 * document, so they won't have to worry about that.
	 * 
	 * @param offset
	 *            the offset of the range of interest
	 * @param length
	 *            the length of the range of interest
	 * @return the partitioning of the range
	 */
	public ITypedRegion[] computePartitioning(int offset, int length) {
		if (fStructuredDocument == null) {
			throw new IllegalStateException("document partitioner is not connected"); //$NON-NLS-1$
		}
		ITypedRegion[] results = null;

		synchronized (cachedPartitions) {
			if ((!cachedPartitions.isInValid) && (offset == cachedPartitions.fOffset) && (length == cachedPartitions.fLength))
				results = cachedPartitions.fPartitions;
		}

		if (results == null) {
			if (length == 0) {
				results = new ITypedRegion[]{getPartition(offset)};
			} else {
				List list = new ArrayList();
				int endPos = offset + length;
				if (endPos > fStructuredDocument.getLength()) {
					// This can occur if the model instance is being
					// changed
					// and everyone's not yet up to date
					return new ITypedRegion[]{createPartition(offset, length, getUnknown())};
				}
				int currentPos = offset;
				IStructuredTypedRegion previousPartition = null;
				while (currentPos < endPos) {
					IStructuredTypedRegion partition = null;
					synchronized (PARTITION_LOCK) {
						internalGetPartition(currentPos, false);
						currentPos += internalReusedTempInstance.getLength();
						
						// check if this partition just continues last one
						// (type is the same),
						// if so, just extend length of last one, not need to
						// create new
						// instance.
						if (previousPartition != null && internalReusedTempInstance.getType().equals(previousPartition.getType())) {
							// same partition type
							previousPartition.setLength(previousPartition.getLength() + internalReusedTempInstance.getLength());
						}
						else {
							// not the same, so add to list
							partition = createNewPartitionInstance();
						}
					}
					if (partition != null) {
						list.add(partition);
						// and make current, previous
						previousPartition = partition;
					}
				}
				results = new ITypedRegion[list.size()];
				list.toArray(results);
			}
			if (results.length > 0) {
				// truncate returned results to requested range
				if (results[0].getOffset() < offset && results[0] instanceof IStructuredRegion) {
					((IStructuredRegion) results[0]).setOffset(offset);
				}
				int lastEnd = results[results.length - 1].getOffset() + results[results.length - 1].getLength();
				if (lastEnd > offset + length && results[results.length - 1] instanceof IStructuredRegion) {
					((IStructuredRegion) results[results.length - 1]).setLength(offset + length - results[results.length - 1].getOffset());
				}
			}
			synchronized (cachedPartitions) {
				cachedPartitions.fLength = length;
				cachedPartitions.fOffset = offset;
				cachedPartitions.fPartitions = results;
				cachedPartitions.isInValid = false;
			}
		}
		return results;
	}

	private void invalidatePartitionCache() {
		synchronized (cachedPartitions) {
			cachedPartitions.isInValid = true;
		}
	}

	/**
	 * Connects the document to the partitioner, i.e. indicates the begin of
	 * the usage of the receiver as partitioner of the given document.
	 */
	public synchronized void connect(IDocument document) {
		if (document instanceof IStructuredDocument) {
			invalidatePartitionCache();
			this.fStructuredDocument = (IStructuredDocument) document;
		} else {
			throw new IllegalArgumentException("This class and API are for Structured Documents only"); //$NON-NLS-1$
		}
	}

	/**
	 * Determines if the given ITextRegionContainer itself contains another
	 * ITextRegionContainer
	 * 
	 * @param ITextRegionContainer
	 * @return boolean
	 */
	protected boolean containsEmbeddedRegion(IStructuredDocumentRegion container) {
		boolean containsEmbeddedRegion = false;

		ITextRegionList regions = container.getRegions();
		for (int i = 0; i < regions.size(); i++) {
			ITextRegion region = regions.get(i);
			if (region instanceof ITextRegionContainer) {
				containsEmbeddedRegion = true;
				break;
			}
		}
		return containsEmbeddedRegion;
	}

	private IStructuredTypedRegion createNewPartitionInstance() {
		synchronized (PARTITION_LOCK) {
			return new SimpleStructuredTypedRegion(internalReusedTempInstance.getOffset(), internalReusedTempInstance.getLength(), internalReusedTempInstance.getType());
		}
	}

	/**
	 * Creates the concrete partition from the given values. Returns a new
	 * instance for each call.
	 * 
	 * Subclasses may override.
	 * 
	 * @param offset
	 * @param length
	 * @param type
	 * @return ITypedRegion
	 * 
	 * TODO: should be protected
	 */
	public IStructuredTypedRegion createPartition(int offset, int length, String type) {
		return new SimpleStructuredTypedRegion(offset, length, type);
	}

	/**
	 * Disconnects the document from the partitioner, i.e. indicates the end
	 * of the usage of the receiver as partitioner of the given document.
	 * 
	 * @see org.eclipse.jface.text.IDocumentPartitioner#disconnect()
	 */
	public synchronized void disconnect() {
		invalidatePartitionCache();
		this.fStructuredDocument = null;
	}

	/**
	 * Informs about a forthcoming document change.
	 * 
	 * @see org.eclipse.jface.text.IDocumentPartitioner#documentAboutToBeChanged(DocumentEvent)
	 */
	public void documentAboutToBeChanged(DocumentEvent event) {
		invalidatePartitionCache();
	}

	/**
	 * The document has been changed. The partitioner updates the set of
	 * regions and returns whether the structure of the document partitioning
	 * has been changed, i.e. whether partitions have been added or removed.
	 * 
	 * @see org.eclipse.jface.text.IDocumentPartitioner#documentChanged(DocumentEvent)
	 */
	public boolean documentChanged(DocumentEvent event) {
		boolean result = false;
		if (event instanceof StructuredDocumentRegionsReplacedEvent) {
			// partitions don't always change while document regions do,
			// but that's the only "quick check" we have.
			// I'm not sure if something more sophisticated will be needed
			// in the future. (dmw, 02/18/04).
			result = true;
		}
		return result;
	}

	protected boolean doParserSpecificCheck(int offset, boolean partitionFound, IStructuredDocumentRegion sdRegion, IStructuredDocumentRegion previousStructuredDocumentRegion, ITextRegion next, ITextRegion previousStart) {
		// this (conceptually) abstract method is not concerned with
		// specific region types
		return false;
	}

	protected IStructuredDocumentRegion getParserSpecificPreviousRegion(IStructuredDocumentRegion currentRegion) {
		return currentRegion != null ? currentRegion.getPrevious() : null;
	}

	/**
	 * Returns the content type of the partition containing the given
	 * character position of the given document. The document has previously
	 * been connected to the partitioner.
	 * 
	 * @see org.eclipse.jface.text.IDocumentPartitioner#getContentType(int)
	 */
	public String getContentType(int offset) {
		return getPartition(offset).getType();
	}

	/**
	 * To be used by default!
	 */
	public String getDefaultPartitionType() {

		return IStructuredPartitions.DEFAULT_PARTITION;
	}

	/**
	 * Returns the set of all possible content types the partitioner supports.
	 * I.e. Any result delivered by this partitioner may not contain a content
	 * type which would not be included in this method's result.
	 * 
	 * @see org.eclipse.jface.text.IDocumentPartitioner#getLegalContentTypes()
	 */
	public java.lang.String[] getLegalContentTypes() {
		if (fSupportedTypes == null) {
			initLegalContentTypes();
		}
		return fSupportedTypes;
	}

	/**
	 * Returns the partition containing the given character position of the
	 * given document. The document has previously been connected to the
	 * partitioner.
	 * 
	 * Note: this shouldn't be called directly by clients, unless they control
	 * the threading that includes modifications to the document. Otherwise
	 * the document could be modified while partitions are being computed. We
	 * advise that clients use the getPartition API directly from the
	 * document, so they won't have to worry about that.
	 * 
	 * 
	 * 
	 * @see org.eclipse.jface.text.IDocumentPartitioner#getPartition(int)
	 */
	public ITypedRegion getPartition(int offset) {
		internalGetPartition(offset, true);
		return createNewPartitionInstance();
	}

	protected String getPartitionFromBlockedText(ITextRegion region, int offset, String result) {
		// parser sensitive code was moved to subclass for quick transition
		// this (conceptually) abstract version isn't concerned with blocked
		// text

		return result;
	}

	protected String getPartitionType(ForeignRegion region, int offset) {
		String tagname = region.getSurroundingTag();
		String result = null;
		if (tagname != null) {
			result = "BLOCK:" + tagname.toUpperCase(Locale.ENGLISH); //$NON-NLS-1$
		} else {
			result = "BLOCK"; //$NON-NLS-1$
		}
		return result;
	}


	protected String getPartitionType(IBlockedStructuredDocumentRegion blockedStructuredDocumentRegion, int offset) {
		String result = null;
		ITextRegionList regions = blockedStructuredDocumentRegion.getRegions();

		// regions should never be null, or hold zero regions, but just in
		// case...
		if (regions != null && regions.size() > 0) {
			if (regions.size() == 1) {
				// if only one, then its a "pure" blocked note.
				// if more than one, then must contain some embedded region
				// container
				ITextRegion blockedRegion = regions.get(0);
				// double check for code safefy, though should always be true
				if (blockedRegion instanceof ForeignRegion) {
					result = getPartitionType((ForeignRegion) blockedRegion, offset);
				}
			} else {
				// must have some embedded region container, so we'll make
				// sure we'll get the appropriate one
				result = getReleventRegionType(blockedStructuredDocumentRegion, offset);
			}
		}
		return result;
	}

	/**
	 * Method getPartitionType.
	 * 
	 * @param region
	 * @return String
	 */
	private String getPartitionType(ITextRegion region) {
		// if it get's to this "raw" level, then
		// must be default.
		return getDefaultPartitionType();
	}

	/**
	 * Returns the partition based on region type. This basically maps from
	 * one region-type space to another, higher level, region-type space.
	 * 
	 * @param region
	 * @param offset
	 * @return String
	 */
	public String getPartitionType(ITextRegion region, int offset) {
		String result = getDefaultPartitionType();
		//		if (region instanceof ContextRegionContainer) {
		//			result = getPartitionType((ITextRegionContainer) region, offset);
		//		} else {
		if (region instanceof ITextRegionContainer) {
			result = getPartitionType((ITextRegionContainer) region, offset);
		}

		result = getPartitionFromBlockedText(region, offset, result);

		return result;

	}

	/**
	 * Similar to method with 'ITextRegion' as argument, except for
	 * RegionContainers, if it has embedded regions, then we need to drill
	 * down and return DocumentPartition based on "lowest level" region type.
	 * For example, in <body id=" <%= object.getID() %>" > The text between
	 * <%= and %> would be a "java region" not an "HTML region".
	 */
	protected String getPartitionType(ITextRegionContainer region, int offset) {
		// TODO this method needs to be 'cleaned up' after refactoring
		// its instanceof logic seems messed up now.
		String result = null;
		if (region != null) {
			ITextRegion coreRegion = region;
			if (coreRegion instanceof ITextRegionContainer) {
				result = getPartitionType((ITextRegionContainer) coreRegion, ((ITextRegionContainer) coreRegion).getRegions(), offset);
			} else {
				result = getPartitionType(region);
			}
		} else {
			result = getPartitionType((ITextRegion) region, offset);
		}

		return result;
	}

	private String getPartitionType(ITextRegionContainer coreRegion, ITextRegionList regions, int offset) {
		String result = null;
		for (int i = 0; i < regions.size(); i++) {
			ITextRegion region = regions.get(i);
			if (coreRegion.containsOffset(region, offset)) {
				result = getPartitionType(region, offset);
				break;
			}
		}
		return result;
	}

	/**
	 * Computes the partition type for the zero-length partition between a
	 * start tag and end tag with the given name regions.
	 * 
	 * @param previousStartTagNameRegion
	 * @param nextEndTagNameRegion
	 * @return String
	 */
	public String getPartitionTypeBetween(IStructuredDocumentRegion previousNode, IStructuredDocumentRegion nextNode) {
		return getDefaultPartitionType();
	}

	/**
	 * Return the ITextRegion at the given offset. For most cases, this will
	 * be the flatNode itself. Should it contain an embedded
	 * ITextRegionContainer, will return the internal region at the offset
	 * 
	 * 
	 * @param flatNode
	 * @param offset
	 * @return ITextRegion
	 */
	private String getReleventRegionType(IStructuredDocumentRegion flatNode, int offset) {
		//		* Note: the original form of this method -- which returned "deep"
		// region, isn't that
		//		* useful, after doing parent elimination refactoring,
		//		* since once the deep region is returned, its hard to get its text
		// or offset without
		//		* proper parent.
		ITextRegion resultRegion = null;
		if (containsEmbeddedRegion(flatNode)) {
			resultRegion = flatNode.getRegionAtCharacterOffset(offset);
			if (resultRegion instanceof ITextRegionContainer) {
				resultRegion = flatNode.getRegionAtCharacterOffset(offset);
				ITextRegionList regions = ((ITextRegionContainer) resultRegion).getRegions();
				for (int i = 0; i < regions.size(); i++) {
					ITextRegion region = regions.get(i);
					if (flatNode.getStartOffset(region) <= offset && offset < flatNode.getEndOffset(region)) {
						resultRegion = region;
						break;
					}
				}
			}
		} else {
			resultRegion = flatNode;
		}
		return resultRegion.getType();
	}

	/**
	 * To be used, instead of default, when there is some thing surprising
	 * about are attempt to partition
	 */
	protected String getUnknown() {
		return IStructuredPartitions.UNKNOWN_PARTITION;
	}

	/**
	 * to be abstract eventually
	 */
	protected void initLegalContentTypes() {
		fSupportedTypes = new String[]{IStructuredPartitions.DEFAULT_PARTITION, IStructuredPartitions.UNKNOWN_PARTITION};
	}

	/**
	 * Returns the partition containing the given character position of the
	 * given document. The document has previously been connected to the
	 * partitioner. If the checkBetween parameter is true, an offset between a
	 * start and end tag will return a zero-length region.
	 */
	private void internalGetPartition(int offset, boolean checkBetween) {
		if (fStructuredDocument == null) {
			throw new IllegalStateException("document partitioner is not connected"); //$NON-NLS-1$
		}

		boolean partitionFound = false;
		int docLength = fStructuredDocument.getLength();
		// get document region type and map to partition type :
		// Note: a partion can be smaller than a flatnode, if that flatnode
		// contains a region container.
		// That's why we need to get "relevent region".
		IStructuredDocumentRegion structuredDocumentRegion = fStructuredDocument.getRegionAtCharacterOffset(offset);
		// flatNode is null if empty document
		// this is king of a "normal case" for empty document
		if (structuredDocumentRegion == null) {
			if (docLength == 0) {
				/*
				 * In order to prevent infinite error loops, this partition
				 * must never have a zero length unless the document is also
				 * zero length
				 */
				setInternalPartition(offset, 0, getDefaultPartitionType());
				partitionFound = true;
			}
			else {
				/*
				 * This case is "unusual". When would region be null, and
				 * document longer than 0. I think this means something's "out
				 * of sync". And we may want to "flag" that fact and just
				 * return one big region of 'unknown', instead of one
				 * character at a time.
				 */
				setInternalPartition(offset, 1, getUnknown());
				partitionFound = true;
			}
		}	
		else if (checkBetween) {
			// dmw: minimizes out to the first if test above
			//			if (structuredDocumentRegion == null && docLength == 0) {
			//				// known special case for an empty document
			//				setInternalPartition(offset, 0, getDefault());
			//				partitionFound = true;
			//			}
			//			else
			if (structuredDocumentRegion.getStartOffset() == offset) {
				IStructuredDocumentRegion previousStructuredDocumentRegion = getParserSpecificPreviousRegion(structuredDocumentRegion);
				if (previousStructuredDocumentRegion != null) {
					ITextRegion next = structuredDocumentRegion.getRegionAtCharacterOffset(offset);
					ITextRegion previousStart = previousStructuredDocumentRegion.getRegionAtCharacterOffset(previousStructuredDocumentRegion.getStartOffset());
					partitionFound = doParserSpecificCheck(offset, partitionFound, structuredDocumentRegion, previousStructuredDocumentRegion, next, previousStart);
				}
			}
		}

		if (!partitionFound && structuredDocumentRegion != null) {
			/* We want the actual ITextRegion and not a possible ITextRegionCollection that
			 * could be returned by IStructuredDocumentRegion#getRegionAtCharacterOffset
			 * This allows for correct syntax highlighting and content assist.
			 */
			DeepRegion resultRegion = getDeepRegionAtCharacterOffset(structuredDocumentRegion, offset);
			partitionFound = isDocumentRegionBasedPartition(structuredDocumentRegion, resultRegion.region, offset);
			if (!partitionFound) {
				if (resultRegion.region != null) {
					String type = getPartitionType(resultRegion.region, offset);
					setInternalPartition(offset, resultRegion.end - offset, type);
				} else {
					// can happen at EOF
					// https://bugs.eclipse.org/bugs/show_bug.cgi?id=224886
					// The unknown type was causing problems with content assist in JSP documents
					setInternalPartition(offset, 1, getDefaultPartitionType());
				}
			}
		}
	}

	private static class DeepRegion {
		int end;
		ITextRegion region;
		DeepRegion(ITextRegion r, int e) {
			region = r;
			end = e;
		}
	}

	/**
	 * <p>Unlike {@link IStructuredDocumentRegion#getRegionAtCharacterOffset(int)} this will dig
	 * into <code>ITextRegionCollection</code> to find the region containing the given offset</p>
	 * 
	 * @param region the containing region of the given <code>offset</code>
	 * @param offset to the overall offset in the document.
	 * @return the <code>ITextRegion</code> containing the given <code>offset</code>, will never be
	 * a <code>ITextRegionCollextion</code>
	 */
	private DeepRegion getDeepRegionAtCharacterOffset(IStructuredDocumentRegion region, int offset) {
		ITextRegion text = region.getRegionAtCharacterOffset(offset);
		int end = region.getStartOffset();
		if (text != null)
			end += text.getStart();
		while (text instanceof ITextRegionCollection) {
			text = ((ITextRegionCollection) text).getRegionAtCharacterOffset(offset);
			end += text.getStart();
		}
		if (text != null)
			end += text.getLength();
		return new DeepRegion(text, end);
	}

	/**
	 * Provides for a per-StructuredDocumentRegion override selecting the
	 * partition type using more than just a single ITextRegion.
	 * 
	 * @param structuredDocumentRegion
	 *            the StructuredDocumentRegion
	 * @param containedChildRegion
	 *            an ITextRegion within the given StructuredDocumentRegion
	 *            that would normally determine the partition type by itself
	 * @param offset
	 *            the document offset
	 * @return true if the partition type will be overridden, false to
	 *         continue normal processing
	 */
	protected boolean isDocumentRegionBasedPartition(IStructuredDocumentRegion structuredDocumentRegion, ITextRegion containedChildRegion, int offset) {
		return false;
	}

	public IDocumentPartitioner newInstance() {
		return new StructuredTextPartitioner();
	}

	protected void setInternalPartition(int offset, int length, String type) {
		synchronized (PARTITION_LOCK) {
			internalReusedTempInstance.setOffset(offset);
			internalReusedTempInstance.setLength(length);
			internalReusedTempInstance.setType(type);
		}
	}

}
