/*******************************************************************************
 * Copyright (c) 2000, 2016 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.corext.dom;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;

import org.eclipse.jdt.core.IBuffer;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.ITypeRoot;
import org.eclipse.jdt.core.JavaCore;
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.core.manipulation.JavaManipulation;

/**
 * Wraps a scanner and offers convenient methods for finding tokens
 */
public class TokenScanner {

	public static final int END_OF_FILE= 20001;
	public static final int LEXICAL_ERROR= 20002;
	public static final int DOCUMENT_ERROR= 20003;

	private IScanner fScanner;
	private IDocument fDocument;
	private int fEndPosition;

	/**
	 * Creates a TokenScanner
	 * @param scanner The scanner to be wrapped. The scanner has to support line information
	 * if the comment position methods are used.
	 */
	public TokenScanner(IScanner scanner) {
		this(scanner, null);
	}

	/**
	 * Creates a TokenScanner
	 * @param scanner The scanner to be wrapped
	 * @param document The document used for line information if specified
	 */
	public TokenScanner(IScanner scanner, IDocument document) {
		fScanner= scanner;
		fEndPosition= fScanner.getSource().length - 1;
		fDocument= document;
	}

	/**
	 * Creates a TokenScanner
	 * @param document The textbuffer to create the scanner on
	 * @param project the current Java project
	 */
	public TokenScanner(IDocument document, IJavaProject project) {
		String sourceLevel= project.getOption(JavaCore.COMPILER_SOURCE, true);
		String complianceLevel= project.getOption(JavaCore.COMPILER_COMPLIANCE, true);
		fScanner= ToolFactory.createScanner(true, false, false, sourceLevel, complianceLevel); // no line info required
		fScanner.setSource(document.get().toCharArray());
		fDocument= document;
		fEndPosition= fScanner.getSource().length - 1;
	}

	/**
	 * Creates a TokenScanner
	 * @param typeRoot The type root to scan on
	 * @throws CoreException thrown if the buffer cannot be accessed
	 */
	public TokenScanner(ITypeRoot typeRoot) throws CoreException {
		IJavaProject project= typeRoot.getJavaProject();
		IBuffer buffer= typeRoot.getBuffer();
		if (buffer == null) {
			throw new CoreException(createError(DOCUMENT_ERROR, "Element has no source", null)); //$NON-NLS-1$
		}
		String sourceLevel= project.getOption(JavaCore.COMPILER_SOURCE, true);
		String complianceLevel= project.getOption(JavaCore.COMPILER_COMPLIANCE, true);
		fScanner= ToolFactory.createScanner(true, false, true, sourceLevel, complianceLevel); // line info required

		fScanner.setSource(buffer.getCharacters());
		fDocument= null; // use scanner for line information
		fEndPosition= fScanner.getSource().length - 1;
	}

	/**
	 * Returns the wrapped scanner
	 * @return IScanner
	 */
	public IScanner getScanner() {
		return fScanner;
	}

	/**
	 * Sets the scanner offset to the given offset.
	 * @param offset The offset to set
	 */
	public void setOffset(int offset) {
		fScanner.resetTo(offset, fEndPosition);
	}

	/**
	 * @return Returns the offset after the current token
	 */
	public int getCurrentEndOffset() {
		return fScanner.getCurrentTokenEndPosition() + 1;
	}

	/**
	 * @return Returns the start offset of the current token
	 */
	public int getCurrentStartOffset() {
		return fScanner.getCurrentTokenStartPosition();
	}

	/**
	 * @return Returns the length of the current token
	 */
	public int getCurrentLength() {
		return getCurrentEndOffset() - getCurrentStartOffset();
	}

	/**
	 * Reads the next token.
	 * @param ignoreComments If set, comments will be overread
	 * @return Return the token id.
	 * @exception CoreException Thrown when the end of the file has been reached (code END_OF_FILE)
	 * or a lexical error was detected while scanning (code LEXICAL_ERROR)
	 */
	public int readNext(boolean ignoreComments) throws CoreException {
		int curr= 0;
		do {
			try {
				curr= fScanner.getNextToken();
				if (curr == ITerminalSymbols.TokenNameEOF) {
					throw new CoreException(createError(END_OF_FILE, "End Of File", null)); //$NON-NLS-1$
				}
			} catch (InvalidInputException e) {
				throw new CoreException(createError(LEXICAL_ERROR, e.getMessage(), e));
			}
		} while (ignoreComments && isComment(curr));
		return curr;
	}

	/**
	 * Reads the next token.
	 * @param ignoreComments If set, comments will be overread.
	 * @return Return the token id.
	 * @exception CoreException Thrown when the end of the file has been reached (code END_OF_FILE)
	 * or a lexical error was detected while scanning (code LEXICAL_ERROR)
	 */
	private int readNextWithEOF(boolean ignoreComments) throws CoreException {
		int curr= 0;
		do {
			try {
				curr= fScanner.getNextToken();
			} catch (InvalidInputException e) {
				throw new CoreException(createError(LEXICAL_ERROR, e.getMessage(), e));
			}
		} while (ignoreComments && isComment(curr));
		return curr;
	}

	/**
	 * Reads the next token from the given offset.
	 * @param offset The offset to start reading from.
	 * @param ignoreComments If set, comments will be overread.
	 * @return Returns the token id.
	 * @exception CoreException Thrown when the end of the file has been reached (code END_OF_FILE)
	 * or a lexical error was detected while scanning (code LEXICAL_ERROR)
	 */
	public int readNext(int offset, boolean ignoreComments) throws CoreException {
		setOffset(offset);
		return readNext(ignoreComments);
	}

	/**
	 * Reads the next token from the given offset and returns the start offset of the token.
	 * @param offset The offset to start reading from.
	 * @param ignoreComments If set, comments will be overread
	 * @return Returns the start position of the next token.
	 * @exception CoreException Thrown when the end of the file has been reached (code END_OF_FILE)
	 * or a lexical error was detected while scanning (code LEXICAL_ERROR)
	 */
	public int getNextStartOffset(int offset, boolean ignoreComments) throws CoreException {
		readNext(offset, ignoreComments);
		return getCurrentStartOffset();
	}

	/**
	 * Reads the next token from the given offset and returns the offset after the token.
	 * @param offset The offset to start reading from.
	 * @param ignoreComments If set, comments will be overread
	 * @return Returns the end position of the next token.
	 * @exception CoreException Thrown when the end of the file has been reached (code END_OF_FILE)
	 * or a lexical error was detected while scanning (code LEXICAL_ERROR)
	 */
	public int getNextEndOffset(int offset, boolean ignoreComments) throws CoreException {
		readNext(offset, ignoreComments);
		return getCurrentEndOffset();
	}

	/**
	 * Reads until a token is reached.
	 * @param tok The token to read to.
	 * @exception CoreException Thrown when the end of the file has been reached (code END_OF_FILE)
	 * or a lexical error was detected while scanning (code LEXICAL_ERROR)
	 */
	public void readToToken(int tok) throws CoreException {
		int curr= 0;
		do {
			curr= readNext(false);
		} while (curr != tok);
	}

	/**
	 * Reads until a token is reached, starting from the given offset.
	 * @param tok The token to read to.
	 * @param offset The offset to start reading from.
	 * @exception CoreException Thrown when the end of the file has been reached (code END_OF_FILE)
	 * or a lexical error was detected while scanning (code LEXICAL_ERROR)
	 */
	public void readToToken(int tok, int offset) throws CoreException {
		setOffset(offset);
		readToToken(tok);
	}

	/**
	 * Reads from the given offset until a token is reached and returns the start offset of the token.
	 * @param token The token to be found.
	 * @param startOffset The offset to start reading from.
	 * @return Returns the start position of the found token.
	 * @exception CoreException Thrown when the end of the file has been reached (code END_OF_FILE)
	 * or a lexical error was detected while scanning (code LEXICAL_ERROR)
	 */
	public int getTokenStartOffset(int token, int startOffset) throws CoreException {
		readToToken(token, startOffset);
		return getCurrentStartOffset();
	}

	/**
	 * Reads from the given offset until a token is reached and returns the offset after the token.
	 * @param token The token to be found.
	 * @param startOffset Offset to start reading from
	 * @return Returns the end position of the found token.
	 * @exception CoreException Thrown when the end of the file has been reached (code END_OF_FILE)
	 * or a lexical error was detected while scanning (code LEXICAL_ERROR)
	 */
	public int getTokenEndOffset(int token, int startOffset) throws CoreException {
		readToToken(token, startOffset);
		return getCurrentEndOffset();
	}

	/**
	 * Reads from the given offset until a token is reached and returns the offset after the previous token.
	 * @param token The token to be found.
	 * @param startOffset The offset to start scanning from.
	 * @return Returns the end offset of the token previous to the given token.
	 * @exception CoreException Thrown when the end of the file has been reached (code END_OF_FILE)
	 * or a lexical error was detected while scanning (code LEXICAL_ERROR)
	 */
	public int getPreviousTokenEndOffset(int token, int startOffset) throws CoreException {
		setOffset(startOffset);
		int res= startOffset;
		int curr= readNext(false);
		while (curr != token) {
			res= getCurrentEndOffset();
			curr= readNext(false);
		}
		return res;
	}

	/**
	 * Evaluates the start offset of comments directly ahead of a token specified by its start offset
	 *
	 * @param lastPos An offset to before the node start offset. Can be 0 but better is the end location of the previous node.
	 * @param nodeStart Start offset of the node to find the comments for.
	 * @return Returns the start offset of comments directly ahead of a token.
	 * @exception CoreException Thrown when a lexical error was detected while scanning (code LEXICAL_ERROR)
	 */
	public int getTokenCommentStart(int lastPos, int nodeStart) throws CoreException {
		setOffset(lastPos);

		int prevEndPos= lastPos;
		int prevEndLine= prevEndPos > 0 ? getLineOfOffset(prevEndPos - 1) : 0;
		int nodeLine= getLineOfOffset(nodeStart);

		int res= -1;

		int curr= readNextWithEOF(false);
		int currStartPos= getCurrentStartOffset();
		int currStartLine= getLineOfOffset(currStartPos);
		while (curr != ITerminalSymbols.TokenNameEOF && nodeStart > currStartPos) {
			if (TokenScanner.isComment(curr)) {
				int linesDifference= currStartLine - prevEndLine;
				if ((linesDifference > 1) || (res == -1 && (linesDifference != 0 || nodeLine == currStartLine))) {
					res= currStartPos; // begin new
				}
			} else {
				res= -1;
			}

			if (curr == ITerminalSymbols.TokenNameCOMMENT_LINE) {
				prevEndLine= currStartLine;
			} else {
				prevEndLine= getLineOfOffset(getCurrentEndOffset() - 1);
			}
			curr= readNextWithEOF(false);
			currStartPos= getCurrentStartOffset();
			currStartLine= getLineOfOffset(currStartPos);
		}
		if (res == -1 || curr == ITerminalSymbols.TokenNameEOF) {
			return nodeStart;
		}
		if (currStartLine - prevEndLine > 1) {
			return nodeStart;
		}
		return res;
	}

	/**
	 * Looks for comments after a node and returns the end position of the comment still belonging to the node.
	 * @param nodeEnd The end position of the node
	 * @param nextTokenStart The start positoion of the next node. Optional, can be -1
	 * the line information shoould be taken from the scanner object
	 * @return Returns returns the end position of the comment still belonging to the node.
	 * @exception CoreException Thrown when the end of the file has been reached (code END_OF_FILE)
	 * or a lexical error was detected while scanning (code LEXICAL_ERROR)
	 */
	public int getTokenCommentEnd(int nodeEnd, int nextTokenStart) throws CoreException {
		// assign comments to the previous comments as long they are all on the same line as the
		// node end position or if they are on the next line but there is a separation from the next
		// node
		// } //aa
		// // aa
		//
		// // bb
		// public void b...
		//
		// } /* cc */ /*
		// cc/*
		// /*dd*/
		// public void d...

		int prevEndLine= getLineOfOffset(nodeEnd - 1);
		int prevEndPos= nodeEnd;
		int res= nodeEnd;
		boolean sameLineComment= true;

		setOffset(nodeEnd);


		int curr= readNextWithEOF(false);
		while (curr == ITerminalSymbols.TokenNameCOMMENT_LINE || curr == ITerminalSymbols.TokenNameCOMMENT_BLOCK) {
			int currStartLine= getLineOfOffset(getCurrentStartOffset());
			int linesDifference= currStartLine - prevEndLine;

			if (linesDifference > 1) {
				return prevEndPos; // separated comments
			}

			if (curr == ITerminalSymbols.TokenNameCOMMENT_LINE) {
				prevEndPos= getLineEnd(currStartLine);
				prevEndLine= currStartLine;
			} else {
				prevEndPos= getCurrentEndOffset();
				prevEndLine= getLineOfOffset(prevEndPos - 1);
			}
			if (sameLineComment) {
				if (linesDifference == 0) {
					res= prevEndPos;
				} else {
					sameLineComment= false;
				}
			}
			curr= readNextWithEOF(false);
		}
		if (curr == ITerminalSymbols.TokenNameEOF) {
			return prevEndPos;
		}
		int currStartLine= getLineOfOffset(getCurrentStartOffset());
		int linesDifference= currStartLine - prevEndLine;
		if (linesDifference > 1) {
			return prevEndPos; // separated comments
		}
		return res;
	}

	public int getLineOfOffset(int offset) throws CoreException {
		if (fDocument != null) {
			try {
				return fDocument.getLineOfOffset(offset);
			} catch (BadLocationException e) {
				String message= "Illegal offset: " + offset; //$NON-NLS-1$
				throw new CoreException(createError(DOCUMENT_ERROR, message, e));
			}
		}
		return getScanner().getLineNumber(offset);
	}

	public int getLineEnd(int line) throws CoreException {
		if (fDocument != null) {
			try {
				IRegion region= fDocument.getLineInformation(line);
				return region.getOffset() + region.getLength();
			} catch (BadLocationException e) {
				String message= "Illegal line: " + line; //$NON-NLS-1$
				throw new CoreException(createError(DOCUMENT_ERROR, message, e));
			}
		}
		return getScanner().getLineEnd(line);
	}

	public static boolean isComment(int token) {
		return token == ITerminalSymbols.TokenNameCOMMENT_BLOCK || token == ITerminalSymbols.TokenNameCOMMENT_JAVADOC
			|| token == ITerminalSymbols.TokenNameCOMMENT_LINE;
	}

	public static boolean isModifier(int token) {
		switch (token) {
			case ITerminalSymbols.TokenNamepublic:
			case ITerminalSymbols.TokenNameprotected:
			case ITerminalSymbols.TokenNameprivate:
			case ITerminalSymbols.TokenNamestatic:
			case ITerminalSymbols.TokenNamefinal:
			case ITerminalSymbols.TokenNameabstract:
			case ITerminalSymbols.TokenNamenative:
			case ITerminalSymbols.TokenNamevolatile:
			case ITerminalSymbols.TokenNamestrictfp:
			case ITerminalSymbols.TokenNametransient:
			case ITerminalSymbols.TokenNamesynchronized:
				return true;
			default:
				return false;
		}
	}

	private IStatus createError(int code, String message, Throwable e) {
		return new Status(IStatus.ERROR, JavaManipulation.ID_PLUGIN, code, message, e);
	}
}
