/*******************************************************************************
 * Copyright (c) 2000, 2017 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
 *     Holger Schill - Bug 455199 - [debug] Debugging doesn't work properly when inner classes are used
 *******************************************************************************/
package org.eclipse.jdt.internal.debug.eval.ast.engine;

import java.util.Iterator;
import java.util.List;

import org.eclipse.jdt.core.Signature;
import org.eclipse.jdt.debug.core.IJavaReferenceType;
import org.eclipse.jdt.internal.debug.core.model.JDIReferenceType;

import com.sun.jdi.ClassNotLoadedException;
import com.sun.jdi.ClassNotPreparedException;
import com.sun.jdi.ClassType;
import com.sun.jdi.Field;
import com.sun.jdi.InterfaceType;
import com.sun.jdi.Method;
import com.sun.jdi.ReferenceType;
import com.sun.jdi.Type;

public class BinaryBasedSourceGenerator {

	private static final String RUN_METHOD_NAME = "___run"; //$NON-NLS-1$
	private static final String EVAL_METHOD_NAME = "___eval"; //$NON-NLS-1$
	private static final String ANONYMOUS_CLASS_NAME = "___EvalClass"; //$NON-NLS-1$

	private String[] fLocalVariableTypeNames;

	private String[] fLocalVariableNames;

	private boolean fIsInStaticMethod;

	private StringBuilder fSource;

	private int fRunMethodStartOffset;
	private int fRunMethodLength;
	private int fCodeSnippetPosition;

	private String fCompilationUnitName;

	/**
	 * Level of source code to generate (major, minor). For example 1 and 4
	 * indicates 1.4.
	 */
	private int fSourceMajorLevel;
	private int fSourceMinorLevel;

	public BinaryBasedSourceGenerator(String[] localTypesNames,
			String[] localVariables, boolean isInStaticMethod,
			String sourceLevel) {
		fLocalVariableTypeNames = localTypesNames;
		fLocalVariableNames = localVariables;
		fIsInStaticMethod = isInStaticMethod;
		int index = sourceLevel.indexOf('.');
		String num;
		if (index != -1) {
			num = sourceLevel.substring(0, index);
		} else {
			num = sourceLevel;
		}
		fSourceMajorLevel = Integer.parseInt(num);
		if (index != -1) {
			num = sourceLevel.substring(index + 1);
			fSourceMinorLevel = Integer.parseInt(num);
		} else {
			fSourceMinorLevel = 0;
		}
	}

	/**
	 * Build source for an object value (instance context)
	 */
	public void buildSource(JDIReferenceType referenceType) {
		ReferenceType reference = (ReferenceType) referenceType
				.getUnderlyingType();
		fSource = buildTypeDeclaration(reference, buildRunMethod(reference),
				null);
	}

	/**
	 * Build source for a class type (static context)
	 */
	public void buildSourceStatic(IJavaReferenceType type) {
		Type underlyingType = ((JDIReferenceType) type).getUnderlyingType();
		if (!(underlyingType instanceof ReferenceType)) {
			return;
		}
		ReferenceType refType = (ReferenceType) underlyingType;
		fSource = buildTypeDeclaration(refType, buildRunMethod(refType), null,
				false);
		String packageName = getPackageName(refType.name());
		if (packageName != null) {
			fSource.insert(0, "package " + packageName + ";\n"); //$NON-NLS-1$ //$NON-NLS-2$
			fCodeSnippetPosition += 10 + packageName.length();
		}
		fCompilationUnitName = getSimpleName(refType.name());
	}

	protected String getUniqueMethodName(String methodName, ReferenceType type) {
		List<Method> methods = type.methodsByName(methodName);
		while (!methods.isEmpty()) {
			methodName += '_';
			methods = type.methodsByName(methodName);
		}
		return methodName;
	}

	private StringBuilder buildRunMethod(ReferenceType type) {
		StringBuilder source = new StringBuilder();

		if (isInStaticMethod()) {
			source.append("static "); //$NON-NLS-1$
		}

		source.append("void "); //$NON-NLS-1$
		source.append(getUniqueMethodName(RUN_METHOD_NAME, type));
		source.append('(');
		for (int i = 0, length = fLocalVariableNames.length; i < length; i++) {
			source.append(getDotName(fLocalVariableTypeNames[i]));
			source.append(' ');
			source.append(fLocalVariableNames[i]);
			if (i + 1 < length)
			 {
				source.append(", "); //$NON-NLS-1$
			}
		}
		source.append(") throws Throwable {"); //$NON-NLS-1$
		source.append('\n');
		fCodeSnippetPosition = source.length();
		fRunMethodStartOffset = fCodeSnippetPosition;

		source.append('\n');
		source.append('}').append('\n');
		fRunMethodLength = source.length();
		return source;
	}

	private StringBuilder buildTypeDeclaration(ReferenceType referenceType,
			StringBuilder buffer, String nestedTypeName) {

		Field thisField = null;

		for (Field field : referenceType.visibleFields()) {
			if (field.name().startsWith("this$")) { //$NON-NLS-1$
				thisField = field;
				break;
			}
		}

		StringBuilder source = buildTypeDeclaration(referenceType, buffer,
				nestedTypeName, thisField != null);

		if (thisField == null) {
			String packageName = getPackageName(referenceType.name());
			if (packageName != null) {
				source.insert(0, "package " + packageName + ";\n"); //$NON-NLS-1$ //$NON-NLS-2$
				fCodeSnippetPosition += 10 + packageName.length();
			}
			if (isAnonymousTypeName(referenceType.name())) {
				fCompilationUnitName = ANONYMOUS_CLASS_NAME;
			} else {
				fCompilationUnitName = getSimpleName(referenceType.name());
			}
		} else {
			try {
				return buildTypeDeclaration((ReferenceType) thisField.type(),
						source, referenceType.name());
			} catch (ClassNotLoadedException e) {
			}
		}

		return source;
	}

	private StringBuilder buildTypeDeclaration(ReferenceType referenceType,
			StringBuilder buffer, String nestedTypeName,
			boolean hasEnclosingInstance) {
		StringBuilder source = new StringBuilder();

		String typeName = referenceType.name();

		boolean isAnonymousType = isAnonymousTypeName(typeName);

		if (isAnonymousType) {
			ClassType classType = (ClassType) referenceType;

			List<InterfaceType> interfaceList = classType.interfaces();
			String superClassName = classType.superclass().name();
			if (hasEnclosingInstance) {
				source.append("void "); //$NON-NLS-1$
				source.append(getUniqueMethodName(EVAL_METHOD_NAME,
						referenceType));
				source.append("() {\nnew "); //$NON-NLS-1$
				if (!interfaceList.isEmpty()) {
					source.append(getDotName(interfaceList
							.get(0).name()));
				} else {
					source.append(getDotName(superClassName));
				}
				source.append("()"); //$NON-NLS-1$
			} else {
				source.append("public class ").append(ANONYMOUS_CLASS_NAME).append(" "); //$NON-NLS-1$ //$NON-NLS-2$
				if (!interfaceList.isEmpty()) {
					source.append(" implements ").append(getDotName(interfaceList.get(0).name())); //$NON-NLS-1$
				} else {
					source.append(" extends ").append(getDotName(superClassName)); //$NON-NLS-1$
				}
			}

		} else {
			if (referenceType.isFinal()) {
				source.append("final "); //$NON-NLS-1$
			}

			if (referenceType.isStatic()) {
				source.append("static "); //$NON-NLS-1$
			}

			if (referenceType instanceof ClassType) {
				ClassType classType = (ClassType) referenceType;

				if (classType.isAbstract()) {
					source.append("abstract "); //$NON-NLS-1$
				}

				source.append("class "); //$NON-NLS-1$

				source.append(getSimpleName(typeName)).append(' ');

				String genericSignature = referenceType.genericSignature();
				if (genericSignature != null
						&& isSourceLevelGreaterOrEqual(1, 5)) {
					String[] typeParameters = Signature
							.getTypeParameters(genericSignature);
					if (typeParameters.length > 0) {
						source.append('<');
						source.append(Signature
								.getTypeVariable(typeParameters[0]));
						String[] typeParameterBounds = Signature
								.getTypeParameterBounds(typeParameters[0]);
						source.append(" extends ").append(Signature.toString(typeParameterBounds[0]).replace('/', '.')); //$NON-NLS-1$
						for (int i = 1; i < typeParameterBounds.length; i++) {
							source.append(" & ").append(Signature.toString(typeParameterBounds[i]).replace('/', '.')); //$NON-NLS-1$
						}
						for (int j = 1; j < typeParameters.length; j++) {
							source.append(',')
									.append(Signature
											.getTypeVariable(typeParameters[j]));
							typeParameterBounds = Signature
									.getTypeParameterBounds(typeParameters[j]);
							source.append(" extends ").append(Signature.toString(typeParameterBounds[0]).replace('/', '.')); //$NON-NLS-1$
							for (int i = 1; i < typeParameterBounds.length; i++) {
								source.append(" & ").append(Signature.toString(typeParameterBounds[i]).replace('/', '.')); //$NON-NLS-1$
							}
						}
						source.append("> "); //$NON-NLS-1$
					}
					String[] superClassInterfaces = SignatureExt
							.getTypeSuperClassInterfaces(genericSignature);
					int length = superClassInterfaces.length;
					if (length > 0) {
						source.append("extends ").append(Signature.toString(superClassInterfaces[0]).replace('/', '.')); //$NON-NLS-1$
						if (length > 1) {
							source.append(" implements ").append(Signature.toString(superClassInterfaces[1]).replace('/', '.')); //$NON-NLS-1$
							for (int i = 2; i < length; i++) {
								source.append(',')
										.append(Signature
												.toString(superClassInterfaces[1]));
							}
						}
					}
				} else {

					ClassType superClass = classType.superclass();
					if (superClass != null) {
						source.append("extends ").append(getDotName(superClass.name())).append(' '); //$NON-NLS-1$
					}

					List<InterfaceType> interfaces;
					try {
						interfaces = classType.interfaces();
					} catch (ClassNotPreparedException e) {
						return new StringBuilder();
					}
					if (!interfaces.isEmpty()) {
						source.append("implements "); //$NON-NLS-1$
						Iterator<InterfaceType> iterator = interfaces.iterator();
						InterfaceType interface_ = iterator
								.next();
						source.append(getDotName(interface_.name()));
						while (iterator.hasNext()) {
							source.append(',')
									.append(getDotName(iterator
											.next().name()));
						}
					}
				}
			} else if (referenceType instanceof InterfaceType) {
				if (buffer != null) {
					source.append("abstract class "); //$NON-NLS-1$
					source.append(getSimpleName(typeName)).append("___ implements "); //$NON-NLS-1$
					source.append(typeName.replace('$', '.')).append(" {\n"); //$NON-NLS-1$
					fCodeSnippetPosition += source.length();
					source.append(buffer).append("}\n"); //$NON-NLS-1$
					return source;
				}
				source.append("interface "); //$NON-NLS-1$
				source.append(getSimpleName(typeName));
				}
		}

		source.append(" {\n"); //$NON-NLS-1$

		if (buffer != null && !(referenceType instanceof InterfaceType)) {
			fCodeSnippetPosition += source.length();
			source.append(buffer);
		}

		for (Field field : referenceType.fields()) {
			if (!field.name().startsWith("this$")) { //$NON-NLS-1$
				source.append(buildFieldDeclaration(field));
			}
		}

		for (Method method : referenceType.methods()) {
			if (!method.isConstructor() && !method.isStaticInitializer()
				&& !method.isBridge()) {
				source.append(buildMethodDeclaration(method));
			}
		}

		List<ReferenceType> nestedTypes = referenceType.nestedTypes();
		if (nestedTypeName == null) {
			for (ReferenceType nestedType : nestedTypes) {
				if (isADirectInnerType(typeName, nestedType.name())) {
					source.append(buildTypeDeclaration(nestedType, null, null,
						true));
				}
			}
		} else {
			for (ReferenceType nestedType : nestedTypes) {
				if (!nestedTypeName.equals(nestedType.name())
					&& isADirectInnerType(typeName, nestedType.name())) {
					source.append(buildTypeDeclaration(nestedType, null, null,
						true));
				}
			}
		}

		if (isAnonymousType && hasEnclosingInstance) {
			source.append("};\n"); //$NON-NLS-1$
		}

		source.append("}\n"); //$NON-NLS-1$

		return source;
	}

	private StringBuilder buildFieldDeclaration(Field field) {
		StringBuilder source = new StringBuilder();

		if (field.isFinal()) {
			source.append("final "); //$NON-NLS-1$
		}

		if (field.isStatic()) {
			source.append("static "); //$NON-NLS-1$
		}

		if (field.isPublic()) {
			source.append("public "); //$NON-NLS-1$
		} else if (field.isPrivate()) {
			source.append("private "); //$NON-NLS-1$
		} else if (field.isProtected()) {
			source.append("protected "); //$NON-NLS-1$
		}

		source.append(getDotName(field.typeName())).append(' ')
				.append(field.name()).append(';').append('\n');

		return source;
	}

	private StringBuilder buildMethodDeclaration(Method method) {
		StringBuilder source = new StringBuilder();

		if (method.isFinal()) {
			source.append("final "); //$NON-NLS-1$
		}

		if (method.isStatic()) {
			source.append("static "); //$NON-NLS-1$
		}

		if (method.isNative()) {
			source.append("native "); //$NON-NLS-1$
		} else if (method.isAbstract()) {
			source.append("abstract "); //$NON-NLS-1$
		}

		if (method.isPublic()) {
			source.append("public "); //$NON-NLS-1$
		} else if (method.isPrivate()) {
			source.append("private "); //$NON-NLS-1$
		} else if (method.isProtected()) {
			source.append("protected "); //$NON-NLS-1$
		}

		String genericSignature = method.genericSignature();
		if (genericSignature != null && isSourceLevelGreaterOrEqual(1, 5)) {
			String[] typeParameters = Signature
					.getTypeParameters(genericSignature);
			if (typeParameters.length > 0) {
				source.append('<');
				source.append(Signature.getTypeVariable(typeParameters[0]));
				String[] typeParameterBounds = Signature
						.getTypeParameterBounds(typeParameters[0]);
				source.append(" extends ").append(Signature.toString(typeParameterBounds[0]).replace('/', '.')); //$NON-NLS-1$
				for (int i = 1; i < typeParameterBounds.length; i++) {
					source.append(" & ").append(Signature.toString(typeParameterBounds[i]).replace('/', '.')); //$NON-NLS-1$
				}
				for (int j = 1; j < typeParameters.length; j++) {
					source.append(',').append(
							Signature.getTypeVariable(typeParameters[j]));
					typeParameterBounds = Signature
							.getTypeParameterBounds(typeParameters[j]);
					source.append(" extends ").append(Signature.toString(typeParameterBounds[0]).replace('/', '.')); //$NON-NLS-1$
					for (int i = 1; i < typeParameterBounds.length; i++) {
						source.append(" & ").append(Signature.toString(typeParameterBounds[i]).replace('/', '.')); //$NON-NLS-1$
					}
				}
				source.append("> "); //$NON-NLS-1$
			}

			source.append(
					Signature.toString(
							Signature.getReturnType(genericSignature)).replace(
							'/', '.')).append(' ').append(method.name())
					.append('(');

			String[] parameterTypes = Signature
					.getParameterTypes(genericSignature);
			int i = 0;
			if (parameterTypes.length != 0) {
				source.append(
						Signature.toString(parameterTypes[0]).replace('/', '.'))
						.append(" arg").append(i++); //$NON-NLS-1$
				if (method.isVarArgs()) {
					for (int j = 1; j < parameterTypes.length - 1; j++) {
						source.append(',')
								.append(Signature.toString(parameterTypes[j])
										.replace('/', '.'))
								.append(" arg").append(i++); //$NON-NLS-1$
					}
					String typeName = Signature.toString(
							parameterTypes[parameterTypes.length - 1]).replace(
							'/', '.');
					source.append(',')
							.append(typeName.substring(0, typeName.length() - 2))
							.append("...").append(" arg").append(i++); //$NON-NLS-1$ //$NON-NLS-2$
				} else {
					for (int j = 1; j < parameterTypes.length; j++) {
						source.append(',')
								.append(Signature.toString(parameterTypes[j])
										.replace('/', '.'))
								.append(" arg").append(i++); //$NON-NLS-1$
					}
				}
			}
			source.append(')');
		} else {
			source.append(getDotName(method.returnTypeName())).append(' ')
					.append(method.name()).append('(');

			List<String> arguments = method.argumentTypeNames();
			int i = 0;
			if (!arguments.isEmpty()) {
				Iterator<String> iterator = arguments.iterator();
				source.append(getDotName(iterator.next()))
						.append(" arg").append(i++); //$NON-NLS-1$
				if (method.isVarArgs()) {
					while (iterator.hasNext()) {
						source.append(',');
						String argName = getDotName(iterator.next());
						if (!iterator.hasNext()) {
							source.append(
									argName.substring(0, argName.length() - 2))
									.append("..."); //$NON-NLS-1$
						} else {
							source.append(argName);
						}
						source.append(" arg").append(i++); //$NON-NLS-1$
					}
				} else {
					while (iterator.hasNext()) {
						source.append(',')
								.append(getDotName(iterator.next()))
								.append(" arg").append(i++); //$NON-NLS-1$
					}
				}
			}
			source.append(')');
		}

		if (method.isAbstract() || method.isNative()) {
			// No body for abstract and native methods
			source.append(";\n"); //$NON-NLS-1$
		} else {
			source.append('{').append('\n');
			source.append(getReturnStatement(method.returnTypeName()));
			source.append('}').append('\n');
		}

		return source;
	}

	private String getReturnStatement(String returnTypeName) {
		String typeName = getSimpleName(returnTypeName);
		if (typeName.charAt(typeName.length() - 1) == ']') {
			return "return null;\n"; //$NON-NLS-1$
		}
		switch (typeName.charAt(0)) {
		case 'v':
			return ""; //$NON-NLS-1$
		case 'b':
			if (typeName.length() >= 1 && typeName.charAt(1) == 'o') {
				return "return false;\n"; //$NON-NLS-1$
			}
		case 's':
		case 'c':
		case 'i':
		case 'l':
		case 'd':
		case 'f':
			return "return 0;\n"; //$NON-NLS-1$
		default:
			return "return null;\n"; //$NON-NLS-1$
		}
	}

	private String getDotName(String typeName) {
		return typeName.replace('$', '.');
	}

	private boolean isAnonymousTypeName(String typeName) {
		char char0 = getSimpleName(typeName).charAt(0);
		return '0' <= char0 && char0 <= '9';
	}

	private String getSimpleName(String qualifiedName) {
		int pos = qualifiedName.lastIndexOf('$');
		if (pos == -1) {
			pos = qualifiedName.lastIndexOf('.');
		}
		return ((pos == -1) ? qualifiedName : qualifiedName.substring(pos + 1));
	}

	private String getPackageName(String qualifiedName) {
		int pos = qualifiedName.lastIndexOf('.');
		return ((pos == -1) ? null : qualifiedName.substring(0, pos));
	}

	private boolean isADirectInnerType(String typeName, String nestedTypeName) {
		String end = nestedTypeName.substring(typeName.length() + 1);
		return end.indexOf('$') == -1;
	}

	private boolean isInStaticMethod() {
		return fIsInStaticMethod;
	}

	public StringBuilder getSource() {
		return fSource;
	}

	public int getCodeSnippetPosition() {
		return fCodeSnippetPosition;
	}

	public String getCompilationUnitName() {
		return fCompilationUnitName;
	}

	public int getSnippetStart() {
		return fCodeSnippetPosition - 2;
	}

	public int getRunMethodStart() {
		return fCodeSnippetPosition - fRunMethodStartOffset;
	}

	public int getRunMethodLength() {
		return fRunMethodLength;
	}

	/**
	 * Returns whether the source to be generated is greater than or equal to
	 * the given source level.
	 *
	 * @param major
	 *            major level - e.g. 1 from 1.4
	 * @param minor
	 *            minor level - e.g. 4 from 1.4
	 * @return
	 */
	public boolean isSourceLevelGreaterOrEqual(int major, int minor) {
		return (fSourceMajorLevel > major)
				|| (fSourceMajorLevel == major && fSourceMinorLevel >= minor);
	}
}
