/*******************************************************************************
 * Copyright (c) 2000, 2022 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
 *     Jesper Steen Moller - bug 341232
 *******************************************************************************/
package org.eclipse.jdt.internal.debug.eval.ast.engine;

import java.util.Map;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.core.DebugException;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IType;
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.dom.AST;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.debug.core.IJavaReferenceType;
import org.eclipse.jdt.internal.debug.core.JDIDebugPlugin;
import org.eclipse.jdt.internal.debug.core.JavaDebugUtils;
import org.eclipse.jdt.internal.debug.core.model.JDIReferenceType;

/**
 * Creates the source code necessary to evaluate a code snippet. The
 * (simplified) structure of the source is as follows: [package <package name>;]
 * [import <import name>;]* public class
 * <code snippet class name> extends <global variable class name> {
 *   public void run() {
 *     <code snippet>
 *   }
 * }
 */
public class EvaluationSourceGenerator {

	private String fCodeSnippet;

	private String[] fLocalVariableTypeNames;
	private String[] fLocalVariableNames;

	private String fSource;
	private String fCompilationUnitName;
	private int fSnippetStartPosition;
	private int fRunMethodStartPosition;
	private int fRunMethodLength;
	private IJavaProject fJavaProject;

	/**
	 * Rebuild source in presence of external local variables
	 */
	public EvaluationSourceGenerator(String[] localVariableTypesNames,
			String[] localVariableNames, String codeSnippet, IJavaProject javaProject) {
		fLocalVariableTypeNames = localVariableTypesNames;
		fLocalVariableNames = localVariableNames;
		fJavaProject = javaProject;
		fCodeSnippet = getCompleteSnippet(codeSnippet);
	}

	public EvaluationSourceGenerator(String codeSnippet, IJavaProject javaProject) {
		this(new String[0], new String[0], codeSnippet, javaProject);
	}


	/**
	 * Returns the completed codeSnippet by adding required semicolon and
	 * return
	 */
	protected String getCompleteSnippet(String codeSnippet) {
		codeSnippet = codeSnippet.trim(); // remove whitespaces at the end
		boolean inString = false;
		char[] chars = codeSnippet.toCharArray();

		int semicolonIndex = -1;
		int lastSemilcolonIndex = -1;
		for (int i = 0, numChars = chars.length; i < numChars; i++) {
			switch (chars[i]) {
			case '\\':
				if (inString) { // skip the char after an escape char
					i++;
				}
				break;
			case '\"':
			case '\'':
				inString = !inString;
				break;
			case ';':
				if (!inString) { // mark the last 2 semicolon
					semicolonIndex = lastSemilcolonIndex;
					lastSemilcolonIndex = i;
				}
				break;
			}
		}
		StringBuilder wordBuffer = new StringBuilder();
		// if semicolon missing at the end
		if (lastSemilcolonIndex != chars.length-1) {
			semicolonIndex = lastSemilcolonIndex;
		}
		int i ;
		for (i=0; i < chars.length; i++) {
			// copy everything before the last statement or if whitespace
			if (i<= semicolonIndex || Character.isWhitespace(chars[i]) || chars[i] == '}'
					|| (i > 0 && (chars[i - 1] == '}' && (chars[i] == ')' || chars[i] == ',')))) {
				wordBuffer.append(codeSnippet.charAt(i));
			} else {
				break;
			}
		}
		String lastSentence  = codeSnippet.substring(i);
		// don't add return if it there in some condition
		String returnString = "return "; //$NON-NLS-1$
		int index = codeSnippet.lastIndexOf(returnString);
		if (index == -1){
			// empty lastSentence means we have statement block above which requires a empty return
			if (needsReturn(lastSentence)) {
				wordBuffer.append(returnString);
			} else if (lastSentence.isBlank()) {
				wordBuffer.append(returnString + ';');
			}
		}
		else if (index > i){
			if (!Character.isWhitespace(chars[index-1]) || !(Character.isWhitespace(chars[index+6]) || chars[index+6] == '}')){
				if (needsReturn(lastSentence)) {
					wordBuffer.append(returnString);
				}
			}
		} else if (chars[chars.length -1] !='}' && ( (i+7 > chars.length && lastSentence.length() > 1) || (i + 7 <= chars.length && !codeSnippet.substring(i, i+7).equals(returnString)))){
			// add return if last statement does not have return
			if (needsReturn(lastSentence)) {
				wordBuffer.append(returnString);
			}
		}
		for (; i < chars.length; i++) {
			// copy the last statement
			wordBuffer.append(codeSnippet.charAt(i));
		}
		// add semicolon at the end if missing
		if (chars.length > 0) {
			if (chars[chars.length - 1] != ';' && chars[chars.length - 1] != '}') {
				wordBuffer.append(';');
			} else if (chars[chars.length - 1] != ';' && chars[chars.length - 1] == '}') {
				int j = lastSentence.lastIndexOf('=');
				int k = lastSentence.lastIndexOf("=="); //$NON-NLS-1$
				if (j != -1 && (j != k)) {
					wordBuffer.append(';');
				}
			}
		}

		// make sure last char is not at end of a block before treating this as a statement
		if (lastSentence.length() <= 1 && needsReturn(wordBuffer.toString())) {
			wordBuffer.insert(0, returnString).append(' ');
		}

		return wordBuffer.toString();
	}


	/**
	 * Returns <code>true</code> if the snippet required return to be added, or
	 * <code>false</code> if the snippet does not require return to be added.
	 */

	@SuppressWarnings("deprecation")
	private boolean needsReturn(String codeSnippet){
		if ( codeSnippet.length() == 0) {
			return false;
		}
		IScanner scanner = ToolFactory.createScanner(false, false, false, fJavaProject.getOption(JavaCore.COMPILER_SOURCE, true), fJavaProject.getOption(JavaCore.COMPILER_COMPLIANCE, true), true);
		scanner.setSource(codeSnippet.toCharArray());
		int token;
		try {
			token = scanner.getNextToken();
			if (token == ITerminalSymbols.TokenNameEOF) {
				return false;
			}
			int count = 0;
			while (token != ITerminalSymbols.TokenNameEOF) {
				if (count == 0 && (token == ITerminalSymbols.TokenNameIdentifier || token == ITerminalSymbols.TokenNameint || token == ITerminalSymbols.TokenNamefloat ||
						token == ITerminalSymbols.TokenNamedouble || token == ITerminalSymbols.TokenNameboolean || token == ITerminalSymbols.TokenNamechar ||
						token == ITerminalSymbols.TokenNameshort || token == ITerminalSymbols.TokenNamelong)) {
					int currentToken = token;
					token = scanner.getNextToken();
					if ( token == ITerminalSymbols.TokenNameEOF && currentToken == ITerminalSymbols.TokenNameIdentifier ){
						return true;
					}
					count = 1;
				}
				else if ( count == 0 && (token == ITerminalSymbols.TokenNamestatic || token == ITerminalSymbols.TokenNamefinal || token == ITerminalSymbols.TokenNamepackage ||
						token == ITerminalSymbols.TokenNameprivate || token == ITerminalSymbols.TokenNameprotected || token == ITerminalSymbols.TokenNamepublic)){
					token = scanner.getNextToken();
				}
				else if (count == 0 && (token == ITerminalSymbols.TokenNamethrow)){
					return false;
				}
				else if (count == 0 && (token == ITerminalSymbols.TokenNameif || token == ITerminalSymbols.TokenNamewhile
						|| token == ITerminalSymbols.TokenNamedo || token == ITerminalSymbols.TokenNamefor || token == ITerminalSymbols.TokenNametry
						|| token == ITerminalSymbols.TokenNameswitch)) {
					return false;
				}
				else if (count ==1 && (token == ITerminalSymbols.TokenNameLBRACE || token == ITerminalSymbols.TokenNameEQUAL)){
					return true;
				}
				else if (count ==1 && (token == ITerminalSymbols.TokenNameLESS || token == ITerminalSymbols.TokenNameLBRACKET)){
					int currentToken = token;
					token = scanner.getNextToken();
					if ( currentToken == ITerminalSymbols.TokenNameLESS && ( token == ITerminalSymbols.TokenNameIdentifier
							|| (token >= ITerminalSymbols.TokenNameIntegerLiteral && token <= ITerminalSymbols.TokenNameTextBlock))) {
						token = scanner.getNextToken();
						if (token == ITerminalSymbols.TokenNameEOF) {
							return true;
						}
					}
					count = 2;
				}
				else if (count ==2 && (token == ITerminalSymbols.TokenNameGREATER || token == ITerminalSymbols.TokenNameRBRACKET)){
					int currentToken = token;
					token = scanner.getNextToken();
					if ( token == ITerminalSymbols.TokenNameEOF && currentToken == ITerminalSymbols.TokenNameRBRACKET ){
						return true;
					}
					if ( currentToken == ITerminalSymbols.TokenNameGREATER && ( token == ITerminalSymbols.TokenNameIdentifier || (token >= ITerminalSymbols.TokenNameIntegerLiteral && token <= ITerminalSymbols.TokenNameTextBlock))){
						token = scanner.getNextToken();
						if (token == ITerminalSymbols.TokenNameEOF) {
							return true;
						} else if (token == ITerminalSymbols.TokenNameEQUAL) {
							return false;
						}
					}
					count = 3;
				}
				else if (count == 2) {
					token = scanner.getNextToken();
				} else if ( (count == 3 || count == 1 ) && token == ITerminalSymbols.TokenNameIdentifier ){
					 return false;
				}
				else if (count == 0 && token == ITerminalSymbols.TokenNamereturn) {
					return false;
				} else {
					// ignore and continue the tokens specified here, handling following types of statements
					// {return i > 0;}, {/**/};{return true;}
					switch (token) {
						case ITerminalSymbols.TokenNameLBRACE:
						case ITerminalSymbols.TokenNameRBRACE:
						case ITerminalSymbols.TokenNameSEMICOLON:
							token = scanner.getNextToken();
							break;
						default:
							return true;
					}
				}

			}
		}
		catch (InvalidInputException e) {
			// ignore
		}

		return true;
	}

	public String getCompilationUnitName() {
		return fCompilationUnitName;
	}

	public int getSnippetStart() {
		return fSnippetStartPosition;
	}

	public int getRunMethodStart() {
		return fRunMethodStartPosition;
	}

	public int getRunMethodLength() {
		return fRunMethodLength;
	}

	protected void setSnippetStart(int position) {
		fSnippetStartPosition = position;
	}

	protected void setRunMethodStart(int position) {
		fRunMethodStartPosition = position;
	}

	protected void setRunMethodLength(int length) {
		fRunMethodLength = length;
	}

	public String getSnippet() {
		return fCodeSnippet;
	}

	private void createEvaluationSourceFromSource(String source, IType type,
			int line, boolean createInAStaticMethod, IJavaProject project)
			throws DebugException {
		ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
		parser.setSource(source.toCharArray());
		Map<String, String> options = getCompilerOptions(project);
		String sourceLevel = project.getOption(JavaCore.COMPILER_SOURCE, true);
		parser.setCompilerOptions(options);
		CompilationUnit unit = (CompilationUnit) parser.createAST(null);
		SourceBasedSourceGenerator visitor = new SourceBasedSourceGenerator(
				type, line, createInAStaticMethod, fLocalVariableTypeNames,
				fLocalVariableNames, fCodeSnippet, sourceLevel);
		unit.accept(visitor);

		if (visitor.hasError()) {
			throw new DebugException(new Status(IStatus.ERROR,
					JDIDebugPlugin.getUniqueIdentifier(), IStatus.OK,
					visitor.getError(), null));
		}

		String sourceRes = visitor.getSource();
		if (sourceRes == null) {
			return;
		}
		setSource(sourceRes);
		setCompilationUnitName(visitor.getCompilationUnitName());
		setSnippetStart(visitor.getSnippetStart());
		setRunMethodStart(visitor.getRunMethodStart());
		setRunMethodLength(visitor.getRunMethodLength());
	}

	/**
	 * Returns the compiler options used for compiling the expression.
	 * <p>
	 * Turns all errors and warnings into ignore and disables task tags. The
	 * customizable set of compiler options only contains additional Eclipse
	 * options. The standard JDK compiler options can't be changed anyway.
	 *
	 * @param element
	 *            an element (not the Java model)
	 * @return compiler options
	 */
	public static Map<String, String> getCompilerOptions(IJavaProject project) {
		Map<String, String> options = project.getOptions(true);
		for (String key : options.keySet()) {
			String value = options.get(key);
			if (JavaCore.ERROR.equals(value) || JavaCore.WARNING.equals(value) || JavaCore.INFO.equals(value)) {
				options.put(key, JavaCore.IGNORE);
			}
		}
		options.put(JavaCore.COMPILER_TASK_TAGS, ""); //$NON-NLS-1$
		return options;
	}

	private void createEvaluationSourceFromJDIObject(
			BinaryBasedSourceGenerator objectToEvaluationSourceMapper) {

		setCompilationUnitName(objectToEvaluationSourceMapper
				.getCompilationUnitName());
		setSnippetStart(objectToEvaluationSourceMapper.getSnippetStart());
		setRunMethodStart(objectToEvaluationSourceMapper.getRunMethodStart());
		setRunMethodLength(fCodeSnippet.length()
				+ objectToEvaluationSourceMapper.getRunMethodLength());
		setSource(objectToEvaluationSourceMapper
				.getSource()
				.insert(objectToEvaluationSourceMapper.getCodeSnippetPosition(),
						fCodeSnippet).toString());
	}

	private BinaryBasedSourceGenerator getInstanceSourceMapper(
			JDIReferenceType referenceType, boolean isInStaticMethod,
			IJavaProject project) {
		String sourceLevel = project.getOption(JavaCore.COMPILER_SOURCE, true);
		BinaryBasedSourceGenerator objectToEvaluationSourceMapper = new BinaryBasedSourceGenerator(
				fLocalVariableTypeNames, fLocalVariableNames, isInStaticMethod,
				sourceLevel);
		objectToEvaluationSourceMapper.buildSource(referenceType);
		return objectToEvaluationSourceMapper;
	}

	public String getSource(IJavaReferenceType type, int line, IJavaProject javaProject,
			boolean isStatic) throws CoreException {
		if (fSource == null) {
			IType iType = JavaDebugUtils.resolveType(type);
			if (iType != null && !iType.isInterface()) {
				String baseSource = null;
				if (iType.isBinary()) {
					baseSource = iType.getClassFile().getSource();
				} else {
					baseSource = iType.getCompilationUnit().getSource();
				}
				if (baseSource != null) {
					createEvaluationSourceFromSource(baseSource, iType,
							line, isStatic, javaProject);
				}
			}
			if (fSource == null) {
				BinaryBasedSourceGenerator mapper = getInstanceSourceMapper(
						(JDIReferenceType) type, isStatic, javaProject);
				createEvaluationSourceFromJDIObject(mapper);
			}
		}
		return fSource;
	}

	protected void setCompilationUnitName(String name) {
		fCompilationUnitName = name;
	}

	protected void setSource(String source) {
		fSource = source;
	}

}
