/*******************************************************************************
 * Copyright (c) 2006, 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
 *******************************************************************************/
package org.eclipse.jdt.internal.corext.refactoring.binary;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.SubProgressMonitor;

import org.eclipse.jdt.core.Flags;
import org.eclipse.jdt.core.IAnnotatable;
import org.eclipse.jdt.core.IAnnotation;
import org.eclipse.jdt.core.IField;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.IMemberValuePair;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeParameter;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.Signature;

import org.eclipse.jdt.internal.corext.refactoring.Checks;
import org.eclipse.jdt.internal.corext.refactoring.RefactoringCoreMessages;

public class StubCreator {

	/** The internal string buffer */
	protected StringBuffer fBuffer;

	/** Should stubs for private member be generated as well? */
	protected final boolean fStubInvisible;

	public StubCreator(final boolean stubInvisible) {
		fStubInvisible= stubInvisible;
	}

	protected void appendEnumConstants(final IType type) throws JavaModelException {
		final IField[] fields= type.getFields();
		final List<IField> list= new ArrayList<>(fields.length);
		for (IField field : fields) {
			if (Flags.isEnum(field.getFlags()))
				list.add(field);
		}
		for (int index= 0; index < list.size(); index++) {
			if (index > 0)
				fBuffer.append(","); //$NON-NLS-1$
			fBuffer.append(list.get(index).getElementName());
		}
		fBuffer.append(";"); //$NON-NLS-1$
	}

	protected void appendExpression(final String signature) {
		appendExpression(signature, false);
	}

	protected void appendExpression(String signature, boolean castNull) {
		switch (signature.charAt(0)) {
			case Signature.C_BOOLEAN:
				fBuffer.append("false"); //$NON-NLS-1$
				break;
			case Signature.C_BYTE:
			case Signature.C_CHAR:
			case Signature.C_DOUBLE:
			case Signature.C_FLOAT:
			case Signature.C_INT:
			case Signature.C_LONG:
			case Signature.C_SHORT:
				fBuffer.append("0"); //$NON-NLS-1$
				break;
			default:
				if (castNull) {
					fBuffer.append("("); //$NON-NLS-1$
					fBuffer.append(Signature.toString(signature));
					fBuffer.append(")"); //$NON-NLS-1$
				}
				fBuffer.append("null"); //$NON-NLS-1$
				break;
		}
	}

	protected void appendFieldDeclaration(final IField field) throws JavaModelException {
		appendFlags(field);
		fBuffer.append(" "); //$NON-NLS-1$
		final String signature= field.getTypeSignature();
		fBuffer.append(Signature.toString(signature));
		fBuffer.append(" "); //$NON-NLS-1$
		fBuffer.append(field.getElementName());
		if (Flags.isFinal(field.getFlags())) {
			fBuffer.append("="); //$NON-NLS-1$
			appendExpression(signature);
		}
		fBuffer.append(";"); //$NON-NLS-1$
	}

	protected void appendFlags(final IMember member) throws JavaModelException {
		if (member instanceof IAnnotatable) {
			IAnnotation[] annotations= ((IAnnotatable) member).getAnnotations();
			Set<IAnnotation> annotationSet= new HashSet<>();
			for (IAnnotation annotation : annotations) {
				if (!annotationSet.contains(annotation)) {
					appendAnnotation(annotation);
					annotationSet.add(annotation);
				}
			}
			annotationSet.clear();
			annotationSet= null;
		}

		int flags= member.getFlags();
		final int kind= member.getElementType();
		if (kind == IJavaElement.TYPE) {
			flags&= ~Flags.AccSuper;
			final IType type= (IType) member;
			if (!type.isMember())
				flags&= ~Flags.AccPrivate;
			if (Flags.isEnum(flags))
				flags&= ~Flags.AccAbstract;
		}
		if (Flags.isEnum(flags))
			flags&= ~Flags.AccFinal;
		if (kind == IJavaElement.METHOD) {
			flags&= ~Flags.AccVarargs;
			flags&= ~Flags.AccBridge;
		}
		if (flags != 0)
			fBuffer.append(Flags.toString(flags));
	}

	private void appendAnnotation(IAnnotation annotation) throws JavaModelException {
		String name= annotation.getElementName();
		if (!fStubInvisible && name.startsWith("sun.") || name.startsWith("jdk.internal.")) //$NON-NLS-1$ //$NON-NLS-2$
			return; // skip Sun-internal annotations

		fBuffer.append('@');
		fBuffer.append(name.replace('$', '.'));
		fBuffer.append('(');

		IMemberValuePair[] memberValuePairs= annotation.getMemberValuePairs();
		for (IMemberValuePair pair : memberValuePairs) {
			fBuffer.append(pair.getMemberName());
			fBuffer.append('=');
			appendAnnotationValue(pair.getValue(), pair.getValueKind());
			fBuffer.append(',');
		}
		if (memberValuePairs.length > 0)
			fBuffer.deleteCharAt(fBuffer.length() - 1);

		fBuffer.append(')').append('\n');
	}

	private void appendAnnotationValue(Object value, int valueKind) throws JavaModelException {
		if (value instanceof Object[]) {
			Object[] objects= (Object[]) value;
			fBuffer.append('{');
			for (Object object : objects) {
				appendAnnotationValue(object, valueKind);
				fBuffer.append(',');
			}
			if (objects.length > 0)
				fBuffer.deleteCharAt(fBuffer.length() - 1);
			fBuffer.append('}');

		} else {
			switch (valueKind) {
				case IMemberValuePair.K_ANNOTATION:
					appendAnnotation((IAnnotation) value);
					break;
				case IMemberValuePair.K_STRING:
					fBuffer.append('"').append(value).append('"');
					break;

				default:
					fBuffer.append(value);
					break;
			}
		}
	}

	protected void appendMembers(final IType type, final IProgressMonitor monitor) throws JavaModelException {
		try {
			monitor.beginTask(RefactoringCoreMessages.StubCreationOperation_creating_type_stubs, 1);
			for (IJavaElement childJavaElement : type.getChildren()) {
				final IMember child= (IMember) childJavaElement;
				final int flags= child.getFlags();
				final boolean isPrivate= Flags.isPrivate(flags);
				final boolean isDefault= !Flags.isPublic(flags) && !Flags.isProtected(flags) && !isPrivate;
				final boolean stub= fStubInvisible || (!isPrivate && !isDefault);
				if (child instanceof IType) {
					if (stub || "java.lang.invoke.MethodHandle".equals(type.getFullyQualifiedName()) //$NON-NLS-1$
						|| "java.util.concurrent.ConcurrentHashMap$CollectionView".equals(((IType) child).getFullyQualifiedName())) //$NON-NLS-1$
						appendTypeDeclaration((IType) child, new SubProgressMonitor(monitor, 1));
				} else if (child instanceof IField) {
					if (stub && !Flags.isEnum(flags) && !Flags.isSynthetic(flags))
						appendFieldDeclaration((IField) child);
				} else if (child instanceof IMethod) {
					final IMethod method= (IMethod) child;
					final String name= method.getElementName();
					if (method.getDeclaringType().isEnum()) {
						final int count= method.getNumberOfParameters();
						if (count == 0 && "values".equals(name)) //$NON-NLS-1$
							continue;
						if (count == 1 && "valueOf".equals(name) && "Ljava.lang.String;".equals(method.getParameterTypes()[0])) //$NON-NLS-1$ //$NON-NLS-2$
							continue;
						if (method.isConstructor())
							continue;
					}
					boolean skip= !stub || "<clinit>".equals(name); //$NON-NLS-1$
					if (method.isConstructor())
						skip= false;
					skip= skip || Flags.isSynthetic(flags) || Flags.isBridge(flags);
					if (!skip)
						appendMethodDeclaration(method);
				}
				fBuffer.append("\n"); //$NON-NLS-1$
			}
		} finally {
			monitor.done();
		}
	}

	@SuppressWarnings("boxing")
	protected void appendMethodBody(final IMethod method) throws JavaModelException {
		if (method.isConstructor()) {
			final IType declaringType= method.getDeclaringType();
			String superSignature= declaringType.getSuperclassTypeSignature();
			if (superSignature != null) {
				superSignature= Signature.getTypeErasure(superSignature);
				final IType superclass= declaringType.getJavaProject().findType(Signature.getSignatureQualifier(superSignature), Signature.getSignatureSimpleName(superSignature));
				if (superclass != null) {
					final IMethod[] superMethods= superclass.getMethods();

					// collect super constructors by parameter count
					Map<Integer, List<IMethod>> superConstructorsByParamCount= new TreeMap<>();
					boolean multi= false;
					IMethod superConstructor= null;
					for (IMethod superMethod : superMethods) {
						if (superMethod.isConstructor()
							&& !Flags.isPrivate(superMethod.getFlags())
							&& (!Flags.isPackageDefault(superMethod.getFlags()) || declaringType.getPackageFragment().equals(superclass.getPackageFragment()))
							) {
							int paramCount= superMethod.getNumberOfParameters();
							if (paramCount == 0) {
								superConstructor= superMethod;
								break;
							}
							List<IMethod> constructors= superConstructorsByParamCount.get(paramCount);
							if (constructors == null) {
								constructors= new ArrayList<>();
								superConstructorsByParamCount.put(paramCount, constructors);
							}
							constructors.add(superMethod);
						}
					}
					if (superConstructor == null && superConstructorsByParamCount.size() > 0) {
						// look for constructors without exceptions and without parameters
						done: for (List<IMethod> constructors : superConstructorsByParamCount.values()) {
							for (IMethod constructor : constructors) {
								if (constructor.getExceptionTypes().length == 0) {
									superConstructor= constructor;
									multi= constructors.size() != 1;
									if (multi)
										break;
									else
										break done;
								}
								if (superConstructor == null) {
									superConstructor= constructor;
									multi= constructors.size() != 1;
								}
							}
						}
						if (superConstructor == null) {
							// give up, get first
							superConstructor= superConstructorsByParamCount.values().iterator().next().get(0);
							multi= true;
						}
					}
					if (superConstructor != null) {
						final String[] superParameters= superConstructor.getParameterTypes();
						final int paramLength= superParameters.length;
						fBuffer.append("super("); //$NON-NLS-1$
						if (paramLength != 0) {
							for (int index= 0; index < paramLength; index++) {
								if (index > 0)
									fBuffer.append(","); //$NON-NLS-1$
								appendExpression(superParameters[index], multi);
							}
						}
						fBuffer.append(");"); //$NON-NLS-1$
					}
				}
			}
		} else {
			String returnType= method.getReturnType();
			if (!Signature.SIG_VOID.equals(returnType)) {
				fBuffer.append("return "); //$NON-NLS-1$
				appendExpression(returnType);
				fBuffer.append(";"); //$NON-NLS-1$
			}
		}
	}

	protected void appendMethodDeclaration(final IMethod method) throws JavaModelException {
		appendFlags(method);
		fBuffer.append(" "); //$NON-NLS-1$
		final ITypeParameter[] parameters= method.getTypeParameters();
		if (parameters.length > 0) {
			appendTypeParameters(parameters);
			fBuffer.append(" "); //$NON-NLS-1$
		}
		final String returnType= method.getReturnType();
		if (!method.isConstructor()) {
			fBuffer.append(Signature.toString(returnType));
			fBuffer.append(" "); //$NON-NLS-1$
		}
		fBuffer.append(method.getElementName());
		fBuffer.append("("); //$NON-NLS-1$
		final String[] parameterTypes= method.getParameterTypes();
		final int flags= method.getFlags();
		final boolean varargs= Flags.isVarargs(flags);
		final int parameterLength= parameterTypes.length;
		for (int index= 0; index < parameterLength; index++) {
			if (index > 0)
				fBuffer.append(","); //$NON-NLS-1$
			fBuffer.append(Signature.toString(parameterTypes[index]));
			if (varargs && index == parameterLength - 1) {
				final int length= fBuffer.length();
				if (length >= 2 && fBuffer.indexOf("[]", length - 2) >= 0) //$NON-NLS-1$
					fBuffer.setLength(length - 2);
				fBuffer.append("..."); //$NON-NLS-1$
			}
			fBuffer.append(" "); //$NON-NLS-1$
			appendMethodParameterName(method, index);
		}
		fBuffer.append(")"); //$NON-NLS-1$
		final String[] exceptionTypes= method.getExceptionTypes();
		final int exceptionLength= exceptionTypes.length;
		if (exceptionLength > 0)
			fBuffer.append(" throws "); //$NON-NLS-1$
		for (int index= 0; index < exceptionLength; index++) {
			if (index > 0)
				fBuffer.append(","); //$NON-NLS-1$
			fBuffer.append(Signature.toString(exceptionTypes[index]));
		}
		if (Flags.isAnnnotationDefault(flags) ) {
			fBuffer.append(" default "); //$NON-NLS-1$
			IMemberValuePair pair= method.getDefaultValue();
			appendAnnotationValue(pair.getValue(), pair.getValueKind());
		}
		if (Flags.isAbstract(flags) || Flags.isNative(flags))
			fBuffer.append(";"); //$NON-NLS-1$
		else {
			fBuffer.append("{\n"); //$NON-NLS-1$
			appendMethodBody(method);
			fBuffer.append("}"); //$NON-NLS-1$
		}
	}

	/**
	 * Appends a parameter name
	 *
	 * @param method the method
	 * @param index the index of the parameter
	 */
	protected void appendMethodParameterName(IMethod method, int index) {
		fBuffer.append("a"); //$NON-NLS-1$
		fBuffer.append(index);
	}

	protected void appendSuperInterfaceTypes(final IType type) throws JavaModelException {
		final String[] signatures= type.getSuperInterfaceTypeSignatures();
		if (signatures.length > 0) {
			if (type.isInterface())
				fBuffer.append(" extends "); //$NON-NLS-1$
			else
				fBuffer.append(" implements "); //$NON-NLS-1$
		}
		for (int index= 0; index < signatures.length; index++) {
			if (index > 0)
				fBuffer.append(","); //$NON-NLS-1$
			fBuffer.append(Signature.toString(signatures[index]));
		}
	}

	protected void appendTopLevelType(final IType type, IProgressMonitor subProgressMonitor) throws JavaModelException {
		String packageName= type.getPackageFragment().getElementName();
		if (packageName.length() > 0) {
			fBuffer.append("package "); //$NON-NLS-1$
			fBuffer.append(packageName);
			fBuffer.append(";\n"); //$NON-NLS-1$
		}
		appendTypeDeclaration(type, subProgressMonitor);
	}

	protected void appendTypeDeclaration(final IType type, final IProgressMonitor monitor) throws JavaModelException {
		try {
			monitor.beginTask(RefactoringCoreMessages.StubCreationOperation_creating_type_stubs, 1);
			if (type.isAnnotation()) {
				appendFlags(type);
				fBuffer.append(" @interface "); //$NON-NLS-1$
				fBuffer.append(type.getElementName());
				fBuffer.append("{\n"); //$NON-NLS-1$
				appendMembers(type, new SubProgressMonitor(monitor, 1));
				fBuffer.append("}"); //$NON-NLS-1$
			} else if (type.isInterface()) {
				appendFlags(type);
				fBuffer.append(" interface "); //$NON-NLS-1$
				fBuffer.append(type.getElementName());
				appendTypeParameters(type.getTypeParameters());
				appendSuperInterfaceTypes(type);
				fBuffer.append("{\n"); //$NON-NLS-1$
				appendMembers(type, new SubProgressMonitor(monitor, 1));
				fBuffer.append("}"); //$NON-NLS-1$
			} else if (type.isClass()) {
				appendFlags(type);
				fBuffer.append(" class "); //$NON-NLS-1$
				fBuffer.append(type.getElementName());
				appendTypeParameters(type.getTypeParameters());
				final String signature= type.getSuperclassTypeSignature();
				if (signature != null) {
					fBuffer.append(" extends "); //$NON-NLS-1$
					fBuffer.append(Signature.toString(signature));
				}
				appendSuperInterfaceTypes(type);
				fBuffer.append("{\n"); //$NON-NLS-1$
				appendMembers(type, new SubProgressMonitor(monitor, 1));
				fBuffer.append("}"); //$NON-NLS-1$
			} else if (type.isEnum()) {
				appendFlags(type);
				fBuffer.append(" enum "); //$NON-NLS-1$
				fBuffer.append(type.getElementName());
				appendSuperInterfaceTypes(type);
				fBuffer.append("{\n"); //$NON-NLS-1$
				appendEnumConstants(type);
				appendMembers(type, new SubProgressMonitor(monitor, 1));
				fBuffer.append("}"); //$NON-NLS-1$
			}
		} finally {
			monitor.done();
		}
	}

	protected void appendTypeParameters(final ITypeParameter[] parameters) throws JavaModelException {
		final int length= parameters.length;
		if (length > 0)
			fBuffer.append("<"); //$NON-NLS-1$
		for (int index= 0; index < length; index++) {
			if (index > 0)
				fBuffer.append(","); //$NON-NLS-1$
			final ITypeParameter parameter= parameters[index];
			fBuffer.append(parameter.getElementName());
			final String[] bounds= parameter.getBounds();
			final int size= bounds.length;
			if (size > 0)
				fBuffer.append(" extends "); //$NON-NLS-1$
			for (int offset= 0; offset < size; offset++) {
				if (offset > 0)
					fBuffer.append(" & "); //$NON-NLS-1$
				fBuffer.append(bounds[offset]);
			}
		}
		if (length > 0)
			fBuffer.append(">"); //$NON-NLS-1$
	}

	/**
	 * Creates and returns a stub for the given top-level type.
	 *
	 * @param topLevelType the top-level type
	 * @param monitor the progress monitor, can be <code>null</code>
	 * @return the source stub
	 * @throws JavaModelException if this element does not exist or if an exception occurs while
	 *             accessing its corresponding resource
	 */
	public String createStub(IType topLevelType, IProgressMonitor monitor) throws JavaModelException {
		Assert.isTrue(Checks.isTopLevel(topLevelType));
		if (monitor == null)
			monitor= new NullProgressMonitor();

		fBuffer= new StringBuffer(2046);
		appendTopLevelType(topLevelType, monitor);
		String result= fBuffer.toString();
		fBuffer= null;
		return result;
	}

}
