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


import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentExtension3;
import org.eclipse.jface.text.IDocumentPartitioner;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.text.rules.ICharacterScanner;
import org.eclipse.jface.text.rules.IPartitionTokenScanner;
import org.eclipse.jface.text.rules.IToken;
import org.eclipse.jface.text.rules.Token;

import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IWorkbenchPage;

import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;

import org.eclipse.jdt.internal.corext.util.JavaModelUtil;

import org.eclipse.jdt.ui.text.IJavaPartitions;

import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.ui.javaeditor.EditorUtility;

/**
 * This scanner recognizes the JavaDoc comments, Java multi line comments, Java single line comments,
 * Java strings and Java characters.
 */
public class FastJavaPartitionScanner implements IPartitionTokenScanner, IJavaPartitions {

	// states
	private static final int JAVA= 0;
	private static final int SINGLE_LINE_COMMENT= 1;
	private static final int MULTI_LINE_COMMENT= 2;
	private static final int JAVADOC= 3;
	private static final int CHARACTER= 4;
	private static final int STRING= 5;
	private static final int MULTI_LINE_STRING= 6;

	// beginning of prefixes and postfixes
	private static final int NONE= 0;
	private static final int BACKSLASH= 1; // postfix for STRING and CHARACTER
	private static final int SLASH= 2; // prefix for SINGLE_LINE or MULTI_LINE or JAVADOC
	private static final int SLASH_STAR= 3; // prefix for MULTI_LINE_COMMENT or JAVADOC
	private static final int SLASH_STAR_STAR= 4; // prefix for MULTI_LINE_COMMENT or JAVADOC
	private static final int STAR= 5; // postfix for MULTI_LINE_COMMENT or JAVADOC
	private static final int CARRIAGE_RETURN=6; // postfix for STRING, CHARACTER and SINGLE_LINE_COMMENT
	private static final int TRIPLE_QUOTE= 9; // prefix for TextBlock.

	/** The scanner. */
	private final BufferedDocumentScanner fScanner= new BufferedDocumentScanner(1000);	// faster implementation

	private IDocument fCurrentDocument;

	/** The offset of the last returned token. */
	private int fTokenOffset;
	/** The length of the last returned token. */
	private int fTokenLength;

	/** The state of the scanner. */
	private int fState;
	/** The last significant characters read. */
	private int fLast;
	/** The amount of characters already read on first call to nextToken(). */
	private int fPrefixLength;

	// emulate JavaPartitionScanner
	private boolean fEmulate= false;
	private int fJavaOffset;
	private int fJavaLength;

	private IJavaProject fJavaProject;

	private final IToken[] fTokens= new IToken[] {
		new Token(null),
		new Token(JAVA_SINGLE_LINE_COMMENT),
		new Token(JAVA_MULTI_LINE_COMMENT),
		new Token(JAVA_DOC),
		new Token(JAVA_CHARACTER),
		new Token(JAVA_STRING),
		new Token(JAVA_MULTI_LINE_STRING)
	};

	public FastJavaPartitionScanner(boolean emulate) {
	    fEmulate= emulate;
	}

	public FastJavaPartitionScanner() {
	    this(false);
	}

	public FastJavaPartitionScanner(IJavaProject javaProject) {
		this(false);
		fJavaProject= javaProject;
	}

	/*
	 * @see org.eclipse.jface.text.rules.ITokenScanner#nextToken()
	 */
	@Override
	public IToken nextToken() {

		// emulate JavaPartitionScanner
		if (fEmulate) {
			if (fJavaOffset != -1 && fTokenOffset + fTokenLength != fJavaOffset + fJavaLength) {
				fTokenOffset += fTokenLength;
				return fTokens[JAVA];
			} else {
				fJavaOffset= -1;
				fJavaLength= 0;
			}
		}

		fTokenOffset += fTokenLength;
		fTokenLength= fPrefixLength;

		while (true) {
			final int ch= fScanner.read();

			// characters
	 		switch (ch) {
	 		case ICharacterScanner.EOF:
		 		if (fTokenLength > 0) {
		 			fLast= NONE; // ignore last
		 			return preFix(fState, JAVA, NONE, 0);

		 		} else {
		 			fLast= NONE;
		 			fPrefixLength= 0;
					return Token.EOF;
		 		}

	 		case '\r':
	 			// emulate JavaPartitionScanner
	 			if (!fEmulate && fLast != CARRIAGE_RETURN) {
						fLast= CARRIAGE_RETURN;
						fTokenLength++;
	 					continue;

	 			} else {

					switch (fState) {
					case SINGLE_LINE_COMMENT:
					case CHARACTER:
					case STRING:
						if (fTokenLength > 0) {
							IToken token= fTokens[fState];

				 			// emulate JavaPartitionScanner
							if (fEmulate) {
								fTokenLength++;
								fLast= NONE;
								fPrefixLength= 0;
							} else {
								fLast= CARRIAGE_RETURN;
								fPrefixLength= 1;
							}

							fState= JAVA;
							return token;

						} else {
							consume();
							continue;
						}

					default:
						consume();
						continue;
					}
	 			}

	 		case '\n':
				switch (fState) {
				case SINGLE_LINE_COMMENT:
				case CHARACTER:
				case STRING:
					// assert(fTokenLength > 0);
					return postFix(fState);

				default:
					consume();
					continue;
				}

			default:
				if (!fEmulate && fLast == CARRIAGE_RETURN) {
					switch (fState) {
					case SINGLE_LINE_COMMENT:
					case CHARACTER:
					case STRING:

						int last;
						int newState;
						switch (ch) {
						case '/':
							last= SLASH;
							newState= JAVA;
							break;

						case '*':
							last= STAR;
							newState= JAVA;
							break;

						case '\'':
							last= NONE;
							newState= CHARACTER;
							break;

						case '"':
							last= NONE;
							newState= STRING;
							break;

						case '\r':
							last= CARRIAGE_RETURN;
							newState= JAVA;
							break;

						case '\\':
							last= BACKSLASH;
							newState= JAVA;
							break;

						default:
							last= NONE;
							newState= JAVA;
							break;
						}

						fLast= NONE; // ignore fLast
						return preFix(fState, newState, last, 1);

					default:
						break;
					}
				}
			}

			// states
	 		switch (fState) {
	 		case JAVA:
				switch (ch) {
				case '/':
					if (fLast == SLASH) {
						if (fTokenLength - getLastLength(fLast) > 0) {
							return preFix(JAVA, SINGLE_LINE_COMMENT, NONE, 2);
						} else {
							preFix(JAVA, SINGLE_LINE_COMMENT, NONE, 2);
							fTokenOffset += fTokenLength;
							fTokenLength= fPrefixLength;
							break;
						}

					} else {
						fTokenLength++;
						fLast= SLASH;
						break;
					}

				case '*':
					if (fLast == SLASH) {
						if (fTokenLength - getLastLength(fLast) > 0)
							return preFix(JAVA, MULTI_LINE_COMMENT, SLASH_STAR, 2);
						else {
							preFix(JAVA, MULTI_LINE_COMMENT, SLASH_STAR, 2);
							fTokenOffset += fTokenLength;
							fTokenLength= fPrefixLength;
							break;
						}

					} else {
						consume();
						break;
					}

				case '\'':
					fLast= NONE; // ignore fLast
					if (fTokenLength > 0)
						return preFix(JAVA, CHARACTER, NONE, 1);
					else {
						preFix(JAVA, CHARACTER, NONE, 1);
						fTokenOffset += fTokenLength;
						fTokenLength= fPrefixLength;
						break;
					}

				case '"':
					boolean isTextBlockBeginning= scanForTextBlockBeginning();
					if (isTextBlockBeginning) {
						if (fTokenLength - getLastLength(fLast) > 0)
							return preFix(JAVA, MULTI_LINE_STRING, TRIPLE_QUOTE, 3);
						else {
							preFix(JAVA, MULTI_LINE_STRING, TRIPLE_QUOTE, 3);
							fTokenOffset+= fTokenLength;
							fTokenLength= fPrefixLength;
							break;
						}
					}
					else {
						fLast= NONE; // ignore fLast
						if (fTokenLength > 0)
							return preFix(JAVA, STRING, NONE, 1);
						else {
							preFix(JAVA, STRING, NONE, 1);
							fTokenOffset += fTokenLength;
							fTokenLength= fPrefixLength;
							break;
						}
					}

				default:
					consume();
					break;
				}
				break;

	 		case SINGLE_LINE_COMMENT:
				consume();
				break;

	 		case JAVADOC:
				switch (ch) {
				case '/':
					switch (fLast) {
					case SLASH_STAR_STAR:
						return postFix(MULTI_LINE_COMMENT);

					case STAR:
						return postFix(JAVADOC);

					default:
						consume();
						break;
					}
					break;

				case '*':
					fTokenLength++;
					fLast= STAR;
					break;

				default:
					consume();
					break;
				}
				break;

	 		case MULTI_LINE_COMMENT:
				switch (ch) {
				case '*':
					if (fLast == SLASH_STAR) {
						fLast= SLASH_STAR_STAR;
						fTokenLength++;
						fState= JAVADOC;
					} else {
						fTokenLength++;
						fLast= STAR;
					}
					break;

				case '/':
					if (fLast == STAR) {
						return postFix(MULTI_LINE_COMMENT);
					} else {
						consume();
						break;
					}

				default:
					consume();
					break;
				}
				break;

	 		case STRING:
	 			switch (ch) {
	 			case '\\':
					fLast= (fLast == BACKSLASH) ? NONE : BACKSLASH;
					fTokenLength++;
					if (scanForUnicodeSlash()) {
						fTokenLength= fTokenLength + 5;
					}
					break;

				case '\"':
	 				if (fLast != BACKSLASH) {
	 					return postFix(STRING);

		 			} else {
						consume();
						break;
	 				}

		 		default:
					consume();
	 				break;
	 			}
	 			break;

	 		case CHARACTER:
	 			switch (ch) {
				case '\\':
					fLast= (fLast == BACKSLASH) ? NONE : BACKSLASH;
					fTokenLength++;
					if (scanForUnicodeSlash()) {
						fTokenLength= fTokenLength + 5;
					}
					break;

	 			case '\'':
	 				if (fLast != BACKSLASH) {
	 					return postFix(CHARACTER);

	 				} else {
						consume();
		 				break;
	 				}

	 			default:
					consume();
	 				break;
	 			}
	 			break;

	 		case MULTI_LINE_STRING:
				switch (ch) {
				case '\\':
						fLast= (fLast == BACKSLASH) ? NONE : BACKSLASH;
						fTokenLength++;
						if (scanForUnicodeSlash()) {
							fTokenLength= fTokenLength + 5;
						}
						break;
				case '\"':
					if (fLast == BACKSLASH || !scanForTextBlockClose()) {
	 					consume();
	 					break;
		 			} else {
		 				boolean considerEndQuotes= true;
		 				try {
		 					IDocumentPartitioner docPartitioner= fCurrentDocument.getDocumentPartitioner();
		 					if (fCurrentDocument instanceof IDocumentExtension3) {
		 						docPartitioner= ((IDocumentExtension3)fCurrentDocument).getDocumentPartitioner(IJavaPartitions.JAVA_PARTITIONING);
		 					}
		 					if (docPartitioner instanceof FastJavaPartitioner) {
		 						FastJavaPartitioner fjPartitioner= (FastJavaPartitioner) docPartitioner;
		 						if (!fjPartitioner.hasTextBlockSupportedValueChanged()) {
				 					ITypedRegion originalPartition= TextUtilities.getPartition(fCurrentDocument, IJavaPartitions.JAVA_PARTITIONING, fTokenOffset, false);
									ITypedRegion startingPartition= TextUtilities.getPartition(fCurrentDocument, IJavaPartitions.JAVA_PARTITIONING, fTokenOffset+ fTokenLength+2, false);
									fjPartitioner.resetPositionCache();
									if (!originalPartition.equals(startingPartition)) {
										String startingType= startingPartition.getType();
										if (IJavaPartitions.JAVA_MULTI_LINE_STRING.equals(startingType)) {
											considerEndQuotes= false;
										}
									}
		 						}
		 					}
		 					if (!considerEndQuotes) {
		 						for (int i=0; i< 3; i++) {
									fScanner.unread();
								}
		 					}
						} catch (BadLocationException e) {
							//do nothing
						}
		 				if (considerEndQuotes) {
		 					fTokenLength= fTokenLength + 2;
		 				} else {
		 					fTokenLength= fTokenLength - 1;
		 				}
						return postFix(MULTI_LINE_STRING);
					}
				default:
					consume();
					break;
				}
	 			break;
	 		}
		}
 	}

	private boolean scanForUnicodeSlash() {
		int count= 0;
		boolean isUnicodeSlash= false;
		try {
			int ch= fScanner.read();
			count++;
			if (ch == 'u') {
				ch= fScanner.read();
				count++;
				if (ch == '0') {
					ch= fScanner.read();
					count++;
					if (ch == '0') {
						ch= fScanner.read();
						count++;
						if (ch == '5') {
							ch= fScanner.read();
							count++;
							if (ch == 'C') {
								isUnicodeSlash= true;
							}
						}
					}
				}
			}
			if (ch == ICharacterScanner.EOF) {
				count--;
			}
		} catch (IndexOutOfBoundsException e) {
			//let it return false;
		} finally {
			if (!isUnicodeSlash) {
				for (int i= count; i > 0; i--) {
					fScanner.unread();
				}
			}
		}
		return isUnicodeSlash;
	}

	private boolean scanForTextBlockBeginning() {
		if (!isTextBlockSupported()) {
			return false;
		}
		int count= 0;
		boolean isTextBlockBeginning= false;
		try {
			// Don't change the position and current character unless we are certain
			// to be dealing with a text block. For producing all errors like before
			// in case of a valid """ but missing \r or \n, just return false and not
			// throw any error.
			count++;
			int ch= fScanner.read();
			if (ch == '\"') {
				count++;
				ch= fScanner.read();
				if (ch == '\"') {
					count++;
					ch= fScanner.read();
					while (Character.isWhitespace(ch)) {
						switch (ch) {
							case 10: /* \ u000a: LINE FEED               */
							case 13: /* \ u000d: CARRIAGE RETURN         */
								isTextBlockBeginning= true;
								break;
							case ICharacterScanner.EOF:
								count--;
								break;
							default:
								count++;
								ch= fScanner.read();
								break;
						}
						if (isTextBlockBeginning) {
							break;
						}
					}
				} else if (ch == ICharacterScanner.EOF) {
					count--;
				}
			} else if (ch == ICharacterScanner.EOF) {
				count--;
			}
		} catch (IndexOutOfBoundsException e) {
			//let it return false;
		} finally {
			int ignore= 0;
			if (isTextBlockBeginning) {
				ignore= 2;
			}
			for (int i= count - ignore; i > 0; i--) {
				fScanner.unread();
			}
		}
		return isTextBlockBeginning;
	}

	private boolean scanForTextBlockClose() {
		int count= 0;
		boolean isTextBlockEnd= false;
		try {
			count++;
			int ch= fScanner.read();
			if (ch == '\"') {
				count++;
				ch= fScanner.read();
				if (ch == '\"') {
					isTextBlockEnd= true;
				} else if (ch == ICharacterScanner.EOF) {
					count--;
				}

			} else if (ch == ICharacterScanner.EOF) {
				count--;
			}
		} catch (IndexOutOfBoundsException e) {
			//let it return false;
		} finally {
			if (!isTextBlockEnd) {
				for (int i= count; i > 0; i--) {
					fScanner.unread();
				}
			}

		}
		return isTextBlockEnd;
	}

	private static final int getLastLength(int last) {
		switch (last) {
		default:
			return -1;

		case NONE:
			return 0;

		case CARRIAGE_RETURN:
		case BACKSLASH:
		case SLASH:
		case STAR:
			return 1;

		case SLASH_STAR:
			return 2;

		case SLASH_STAR_STAR:
		case TRIPLE_QUOTE:
			return 3;
		}
	}

	private final void consume() {
		fTokenLength++;
		fLast= NONE;
	}

	private final IToken postFix(int state) {
		fTokenLength++;
		fLast= NONE;
		fState= JAVA;
		fPrefixLength= 0;
		return fTokens[state];
	}

	private final IToken preFix(int state, int newState, int last, int prefixLength) {
		// emulate JavaPartitionScanner
		if (fEmulate && state == JAVA && (fTokenLength - getLastLength(fLast) > 0)) {
			fTokenLength -= getLastLength(fLast);
			fJavaOffset= fTokenOffset;
			fJavaLength= fTokenLength;
			fTokenLength= 1;
			fState= newState;
			fPrefixLength= prefixLength;
			fLast= last;
			return fTokens[state];

		} else {
			fTokenLength -= getLastLength(fLast);
			fLast= last;
			fPrefixLength= prefixLength;
			IToken token= fTokens[state];
			fState= newState;
			return token;
		}
	}

	private static int getState(String contentType) {

		if (contentType == null)
			return JAVA;

		switch (contentType) {
		case JAVA_SINGLE_LINE_COMMENT:
			return SINGLE_LINE_COMMENT;
		case JAVA_MULTI_LINE_COMMENT:
			return MULTI_LINE_COMMENT;
		case JAVA_DOC:
			return JAVADOC;
		case JAVA_STRING:
			return STRING;
		case JAVA_CHARACTER:
			return CHARACTER;
		case JAVA_MULTI_LINE_STRING:
			return MULTI_LINE_STRING;
		default:
			return JAVA;
		}
	}

	/*
	 * @see IPartitionTokenScanner#setPartialRange(IDocument, int, int, String, int)
	 */
	@Override
	public void setPartialRange(IDocument document, int offset, int length, String contentType, int partitionOffset) {
		fCurrentDocument= document;
		fScanner.setRange(document, offset, length);
		fTokenOffset= partitionOffset;
		fTokenLength= 0;
		fPrefixLength= offset - partitionOffset;
		fLast= NONE;

		if (offset == partitionOffset) {
			// restart at beginning of partition
			fState= JAVA;
		} else {
			fState= getState(contentType);
		}

		// emulate JavaPartitionScanner
		if (fEmulate) {
			fJavaOffset= -1;
			fJavaLength= 0;
		}
	}

	/*
	 * @see ITokenScanner#setRange(IDocument, int, int)
	 */
	@Override
	public void setRange(IDocument document, int offset, int length) {
		fCurrentDocument= document;
		fScanner.setRange(document, offset, length);
		fTokenOffset= offset;
		fTokenLength= 0;
		fPrefixLength= 0;
		fLast= NONE;
		fState= JAVA;

		// emulate JavaPartitionScanner
		if (fEmulate) {
			fJavaOffset= -1;
			fJavaLength= 0;
		}
	}

	/*
	 * @see ITokenScanner#getTokenLength()
	 */
	@Override
	public int getTokenLength() {
		return fTokenLength;
	}

	/*
	 * @see ITokenScanner#getTokenOffset()
	 */
	@Override
	public int getTokenOffset() {
		return fTokenOffset;
	}

	private void setJavaProject() {
		if (fJavaProject == null) {
			IWorkbenchPage page= null;
			try {
				page= JavaPlugin.getActivePage();
			} catch (IllegalStateException e) {
				//do nothing
			}
			if (page != null) {
				IEditorPart part= page.getActiveEditor();
				if (part != null) {
					IEditorInput editorInput= part.getEditorInput();
					if (editorInput != null) {
						fJavaProject= EditorUtility.getJavaProject(editorInput);
					}
				}
				if (fJavaProject == null) {
					ISelection selection= page.getSelection();
					if (selection instanceof IStructuredSelection && !selection.isEmpty()) {
						Object obj= ((IStructuredSelection) selection).getFirstElement();
						if (obj instanceof IJavaElement) {
							fJavaProject= ((IJavaElement) obj).getJavaProject();
						}
					}
				}
			}
		}
	}

	public boolean isTextBlockSupported() {
		boolean isAllowed= false;
		setJavaProject();
		if (fJavaProject != null) {
			isAllowed= JavaModelUtil.is15OrHigher(fJavaProject);
		}
		return isAllowed;
	}
}
