/*******************************************************************************
 * Copyright (c) 2000, 2012 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.jdt.internal.ui.compare;

import org.eclipse.core.runtime.Assert;

import org.eclipse.compare.contentmergeviewer.ITokenComparator;
import org.eclipse.compare.contentmergeviewer.TokenComparator;
import org.eclipse.compare.rangedifferencer.IRangeComparator;

import org.eclipse.jdt.core.ToolFactory;
import org.eclipse.jdt.core.compiler.IScanner;
import org.eclipse.jdt.core.compiler.ITerminalSymbols;
import org.eclipse.jdt.core.compiler.InvalidInputException;

import org.eclipse.jdt.internal.corext.dom.TokenScanner;


/**
 * A comparator for Java tokens.
 */
public class JavaTokenComparator implements ITokenComparator {

	private static final boolean DEBUG= false;

	private final String fText;
	private int fCount;
	private int[] fStarts;
	private int[] fLengths;

	/**
	 * Creates a token comparator for the given string.
	 *
	 * @param text the text to be tokenized
	 */
	public JavaTokenComparator(String text) {
		Assert.isLegal(text != null);

		fText= text;

		int length= fText.length();
		fStarts= new int[length];
		fLengths= new int[length];
		fCount= 0;

		IScanner scanner= ToolFactory.createScanner(true, true, false, false); // returns comments & whitespace
		scanner.setSource(fText.toCharArray());
		int endPos= 0;
		try {
			int tokenType;
			while ((tokenType= scanner.getNextToken()) != ITerminalSymbols.TokenNameEOF) {
				int start= scanner.getCurrentTokenStartPosition();
				int end= scanner.getCurrentTokenEndPosition()+1;
				// Comments and strings should not be treated as a single token, see https://bugs.eclipse.org/78063
				if (TokenScanner.isComment(tokenType) || tokenType == ITerminalSymbols.TokenNameStringLiteral) {
					// Line comments are often commented code, so lets treat them as code. See https://bugs.eclipse.org/216707
					boolean parseAsJava= tokenType == ITerminalSymbols.TokenNameCOMMENT_LINE;
					int dl= parseAsJava ? getCommentStartTokenLength(tokenType) : 0;
					if (dl > 0)
						recordTokenRange(start, dl);
					parseSubrange(start + dl, text.substring(start + dl, end), parseAsJava);
				} else {
					recordTokenRange(start, end - start);
				}
				endPos= end;
			}
		} catch (InvalidInputException ex) {
			// We couldn't parse part of the input. Fall through and make the rest a single token
		}
		// Workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=13907
		if (endPos < length) {
			recordTokenRange(endPos, length - endPos);
		}
	}

	/**
	 * Records the given token range.
	 *
	 * @param start of the token
	 * @param length length of the token
	 * @since 3.3
	 */
	private void recordTokenRange(int start, int length) {
		fStarts[fCount]= start;
		fLengths[fCount]= length;
		if (DEBUG)
			System.out.println(fCount + " (" + start + "-" + length + ")>" + fText.substring(start, start + length) + "<"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
		fCount++;
	}

	private void parseSubrange(int start, String text, boolean javaCode) {
		if (DEBUG)
			System.out.println("parsingText>" + text + "<(" + start + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$

		ITokenComparator subTokenizer= javaCode ? new JavaTokenComparator(text) : new TokenComparator(text);
		int count= subTokenizer.getRangeCount();
		for (int i= 0; i < count; i++) {
			int subStart= subTokenizer.getTokenStart(i);
			int subLength= subTokenizer.getTokenLength(i);
			if (DEBUG)
				System.out.println("   " + i + " (" + subStart + "-" + subLength + ")"); //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$//$NON-NLS-4$

			recordTokenRange(start + subStart, subLength);
		}
	}

	/**
	 * Returns the length of the token that
	 * initiates the given comment type.
	 *
	 * @param tokenType an {@link ITerminalSymbols} constant
	 * @return the length of the token that start a comment
	 * @since 3.3
	 */
	private static int getCommentStartTokenLength(int tokenType) {
		if (tokenType == ITerminalSymbols.TokenNameCOMMENT_JAVADOC) {
			return 3;
		} else if (tokenType == ITerminalSymbols.TokenNameStringLiteral) {
			return 1;
		} else {
			return 2;
		}
	}

	/**
	 * Returns the number of tokens in the string.
	 *
	 * @return number of token in the string
	 */
	@Override
	public int getRangeCount() {
		return fCount;
	}

	/* (non Javadoc)
	 * see ITokenComparator.getTokenStart
	 */
	@Override
	public int getTokenStart(int index) {
		if (index >= 0 && index < fCount)
			return fStarts[index];
		if (fCount > 0)
			return fStarts[fCount-1] + fLengths[fCount-1];
		return 0;
	}

	/* (non Javadoc)
	 * see ITokenComparator.getTokenLength
	 */
	@Override
	public int getTokenLength(int index) {
		if (index < fCount)
			return fLengths[index];
		return 0;
	}

	/**
	 * Returns <code>true</code> if a token given by the first index
	 * matches a token specified by the other <code>IRangeComparator</code> and index.
	 *
	 * @param thisIndex	the number of the token within this range comparator
	 * @param other the range comparator to compare this with
	 * @param otherIndex the number of the token within the other comparator
	 * @return <code>true</code> if the token are equal
	 */
	@Override
	public boolean rangesEqual(int thisIndex, IRangeComparator other, int otherIndex) {
		if (other != null && getClass() == other.getClass()) {
			JavaTokenComparator tc= (JavaTokenComparator) other;	// safe cast
			int thisLen= getTokenLength(thisIndex);
			int otherLen= tc.getTokenLength(otherIndex);
			if (thisLen == otherLen)
				return fText.regionMatches(false, getTokenStart(thisIndex), tc.fText, tc.getTokenStart(otherIndex), thisLen);
		}
		return false;
	}

	/**
	 * Aborts the comparison if the number of tokens is too large.
	 *
	 * @param length a number on which to base the decision whether to return
	 * 	<code>true</code> or <code>false</code>
	 * @param maxLength another number on which to base the decision whether to return
	 *	<code>true</code> or <code>false</code>
	 * @param other the other <code>IRangeComparator</code> to compare with
	 * @return <code>true</code> to abort a token comparison
	 */
	@Override
	public boolean skipRangeComparison(int length, int maxLength, IRangeComparator other) {

		if (getRangeCount() < 50 || other.getRangeCount() < 50)
			return false;

		if (maxLength < 100)
			return false;

		if (length < 100)
			return false;

		if (maxLength > 800)
			return true;

		if (length < maxLength / 4)
			return false;

		return true;
	}
}
