/*******************************************************************************
 * Copyright (c) 2016 Google, Inc 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:
 *   Stefan Xenos (Google) - Initial implementation
 *******************************************************************************/
package org.aspectj.org.eclipse.jdt.internal.core.nd.indexer;

import java.util.Arrays;
import java.util.Objects;

import org.aspectj.org.eclipse.jdt.internal.compiler.env.ClassSignature;
import org.aspectj.org.eclipse.jdt.internal.compiler.env.EnumConstantSignature;
import org.aspectj.org.eclipse.jdt.internal.compiler.env.IBinaryAnnotation;
import org.aspectj.org.eclipse.jdt.internal.compiler.env.IBinaryElementValuePair;
import org.aspectj.org.eclipse.jdt.internal.compiler.env.IBinaryField;
import org.aspectj.org.eclipse.jdt.internal.compiler.env.IBinaryMethod;
import org.aspectj.org.eclipse.jdt.internal.compiler.env.IBinaryType;
import org.aspectj.org.eclipse.jdt.internal.compiler.env.IBinaryTypeAnnotation;
import org.aspectj.org.eclipse.jdt.internal.compiler.impl.Constant;
import org.aspectj.org.eclipse.jdt.internal.compiler.impl.DoubleConstant;
import org.aspectj.org.eclipse.jdt.internal.compiler.impl.FloatConstant;
import org.aspectj.org.eclipse.jdt.internal.core.nd.util.CharArrayUtils;

public class IndexTester {

	private static final class TypeAnnotationWrapper {
		private IBinaryTypeAnnotation annotation;

		public TypeAnnotationWrapper(IBinaryTypeAnnotation next) {
			this.annotation = next;
		}

		@Override
		public int hashCode() {
			int hashCode;
			int[] typePath = this.annotation.getTypePath();

			hashCode = Arrays.hashCode(typePath);
			hashCode = hashCode * 31 + this.annotation.getTargetType();
			hashCode = hashCode * 31 + this.annotation.getTypeParameterIndex();
			return hashCode;
		}

		@Override
		public String toString() {
			return this.annotation.toString();
		}

		@Override
		public boolean equals(Object obj) {
			if (obj.getClass() != TypeAnnotationWrapper.class) {
				return false;
			}

			TypeAnnotationWrapper wrapper = (TypeAnnotationWrapper) obj;
			IBinaryTypeAnnotation otherAnnotation = wrapper.annotation;

			int[] typePath = this.annotation.getTypePath();
			int[] otherTypePath = otherAnnotation.getTypePath();

			if (!Arrays.equals(typePath, otherTypePath)) {
				return false;
			}

			if (this.annotation.getTargetType() != otherAnnotation.getTargetType()) {
				return false;
			}

			if (this.annotation.getBoundIndex() != otherAnnotation.getBoundIndex()) {
				return false;
			}

			if (this.annotation.getMethodFormalParameterIndex() != otherAnnotation.getMethodFormalParameterIndex()) {
				return false;
			}

			if (this.annotation.getSupertypeIndex() != otherAnnotation.getSupertypeIndex()) {
				return false;
			}

			if (this.annotation.getThrowsTypeIndex() != otherAnnotation.getThrowsTypeIndex()) {
				return false;
			}

			if (this.annotation.getTypeParameterIndex() != otherAnnotation.getTypeParameterIndex()) {
				return false;
			}

			return IndexTester.isEqual(this.annotation.getAnnotation(), otherAnnotation.getAnnotation());
		}
	}

	public static void testType(IBinaryType expected, IBinaryType actual) {
		String contextPrefix = safeString(actual.getName());

		IBinaryTypeAnnotation[] expectedTypeAnnotations = expected.getTypeAnnotations();
		IBinaryTypeAnnotation[] actualTypeAnnotations = actual.getTypeAnnotations();

		compareTypeAnnotations(contextPrefix, expectedTypeAnnotations, actualTypeAnnotations);

		IBinaryAnnotation[] expectedBinaryAnnotations = expected.getAnnotations();
		IBinaryAnnotation[] actualBinaryAnnotations = actual.getAnnotations();

		compareAnnotations(contextPrefix, expectedBinaryAnnotations, actualBinaryAnnotations);

		compareGenericSignatures(contextPrefix + ": The generic signature did not match", //$NON-NLS-1$
				expected.getGenericSignature(), actual.getGenericSignature());

		assertEquals(contextPrefix + ": The enclosing method name did not match", expected.getEnclosingMethod(), //$NON-NLS-1$
				actual.getEnclosingMethod());
		assertEquals(contextPrefix + ": The enclosing method name did not match", expected.getEnclosingTypeName(), //$NON-NLS-1$
				actual.getEnclosingTypeName());

		IBinaryField[] expectedFields = expected.getFields();
		IBinaryField[] actualFields = actual.getFields();

		if (expectedFields != actualFields) {
			if (expectedFields == null && actualFields != null) {
				throw new IllegalStateException(contextPrefix + "Expected fields was null -- actual fields were not"); //$NON-NLS-1$
			}
			if (expectedFields.length != actualFields.length) {
				throw new IllegalStateException(
						contextPrefix + "The expected and actual number of fields did not match"); //$NON-NLS-1$
			}

			for (int fieldIdx = 0; fieldIdx < actualFields.length; fieldIdx++) {
				compareFields(contextPrefix, expectedFields[fieldIdx], actualFields[fieldIdx]);
			}
		}

		// Commented this out because the "expected" values often appear to be invalid paths when the "actual"
		// ones are correct.
		assertEquals("The file name did not match", expected.getFileName(), actual.getFileName()); //$NON-NLS-1$
		assertEquals("The interface names did not match", expected.getInterfaceNames(), actual.getInterfaceNames()); //$NON-NLS-1$

		// Member types are not expected to match during indexing since the index uses discovered cross-references,
		// not the member types encoded in the .class file.
		// expected.getMemberTypes() != actual.getMemberTypes()

		IBinaryMethod[] expectedMethods = expected.getMethods();
		IBinaryMethod[] actualMethods = actual.getMethods();

		if (expectedMethods != actualMethods) {
			if (expectedMethods == null || actualMethods == null) {
				throw new IllegalStateException("One of the method arrays was null"); //$NON-NLS-1$
			}

			if (expectedMethods.length != actualMethods.length) {
				throw new IllegalStateException("The number of methods didn't match"); //$NON-NLS-1$
			}

			for (int i = 0; i < actualMethods.length; i++) {
				IBinaryMethod actualMethod = actualMethods[i];
				IBinaryMethod expectedMethod = expectedMethods[i];

				compareMethods(contextPrefix, expectedMethod, actualMethod);
			}
		}

		assertEquals("The missing type names did not match", expected.getMissingTypeNames(), //$NON-NLS-1$
				actual.getMissingTypeNames());
		assertEquals("The modifiers don't match", expected.getModifiers(), actual.getModifiers()); //$NON-NLS-1$
		assertEquals("The names don't match.", expected.getName(), actual.getName()); //$NON-NLS-1$
		assertEquals("The source name doesn't match", expected.getSourceName(), actual.getSourceName()); //$NON-NLS-1$
		assertEquals("The superclass name doesn't match", expected.getSuperclassName(), actual.getSuperclassName()); //$NON-NLS-1$
		assertEquals("The tag bits don't match.", expected.getTagBits(), actual.getTagBits()); //$NON-NLS-1$

		compareTypeAnnotations(contextPrefix, expected.getTypeAnnotations(), actual.getTypeAnnotations());
	}

	private static <T> void assertEquals(String message, T o1, T o2) {
		if (!isEqual(o1, o2)) {
			throw new IllegalStateException(message + ": expected = " + getString(o1) + ", actual = " + getString(o2)); //$NON-NLS-1$//$NON-NLS-2$
		}
	}

	private static String getString(Object object) {
		if (object instanceof char[]) {
			char[] charArray = (char[]) object;

			return new String(charArray);
		}
		return object.toString();
	}

	static <T> boolean isEqual(T o1, T o2) {
		if (o1 == o2) {
			return true;
		}

		if (o1 == null || o2 == null) {
			return false;
		}

		if (o1 instanceof ClassSignature) {
			if (!(o2 instanceof ClassSignature)) {
				return false;
			}

			ClassSignature sig1 = (ClassSignature) o1;
			ClassSignature sig2 = (ClassSignature) o2;

			return Arrays.equals(sig1.getTypeName(), sig2.getTypeName());
		}

		if (o1 instanceof IBinaryAnnotation) {
			IBinaryAnnotation binaryAnnotation = (IBinaryAnnotation) o1;
			IBinaryAnnotation otherBinaryAnnotation = (IBinaryAnnotation) o2;
			IBinaryElementValuePair[] elementValuePairs = binaryAnnotation.getElementValuePairs();
			IBinaryElementValuePair[] otherElementValuePairs = otherBinaryAnnotation.getElementValuePairs();

			if (elementValuePairs.length != otherElementValuePairs.length) {
				return false;
			}

			for (int idx = 0; idx < elementValuePairs.length; idx++) {
				IBinaryElementValuePair next = elementValuePairs[idx];
				IBinaryElementValuePair otherNext = otherElementValuePairs[idx];

				char[] nextName = next.getName();
				char[] otherNextName = otherNext.getName();

				if (!Arrays.equals(nextName, otherNextName)) {
					return false;
				}

				if (!isEqual(next.getValue(), otherNext.getValue())) {
					return false;
				}
			}
			return true;
		}

		if (o1 instanceof IBinaryTypeAnnotation) {
			IBinaryTypeAnnotation binaryAnnotation = (IBinaryTypeAnnotation)o1;
			IBinaryTypeAnnotation otherBinaryAnnotation = (IBinaryTypeAnnotation)o2;

			return new TypeAnnotationWrapper(binaryAnnotation).equals(new TypeAnnotationWrapper(otherBinaryAnnotation));
		}

		if (o1 instanceof Constant) {
			if (!(o2 instanceof Constant)) {
				return false;
			}

			if (o1 instanceof DoubleConstant && o2 instanceof DoubleConstant) {
				DoubleConstant d1 = (DoubleConstant) o1;
				DoubleConstant d2 = (DoubleConstant) o2;

				if (Double.isNaN(d1.doubleValue()) && Double.isNaN(d2.doubleValue())) {
					return true;
				}
			}

			if (o1 instanceof FloatConstant && o2 instanceof FloatConstant) {
				FloatConstant d1 = (FloatConstant) o1;
				FloatConstant d2 = (FloatConstant) o2;

				if (Float.isNaN(d1.floatValue()) && Float.isNaN(d2.floatValue())) {
					return true;
				}
			}

			Constant const1 = (Constant) o1;
			Constant const2 = (Constant) o2;

			return const1.hasSameValue(const2);
		}

		if (o1 instanceof EnumConstantSignature) {
			if (!(o2 instanceof EnumConstantSignature)) {
				return false;
			}

			EnumConstantSignature enum1 = (EnumConstantSignature) o1;
			EnumConstantSignature enum2 = (EnumConstantSignature) o2;

			return Arrays.equals(enum1.getEnumConstantName(), enum2.getEnumConstantName())
					&& Arrays.equals(enum1.getTypeName(), enum2.getTypeName());
		}

		if (o1 instanceof char[]) {
			char[] c1 = (char[]) o1;
			char[] c2 = (char[]) o2;

			return CharArrayUtils.equals(c1, c2);
		}

		if (o1 instanceof char[][]) {
			char[][] c1 = (char[][]) o1;
			char[][] c2 = (char[][]) o2;

			return CharArrayUtils.equals(c1, c2);
		}

		if (o1 instanceof char[][][]) {
			char[][][] c1 = (char[][][]) o1;
			char[][][] c2 = (char[][][]) o2;

			if (c1.length != c2.length) {
				return false;
			}

			for (int i = 0; i < c1.length; i++) {
				if (!isEqual(c1[i], c2[i])) {
					return false;
				}
			}
			return true;
		}

		if (o1 instanceof Object[]) {
			Object[] a1 = (Object[]) o1;
			Object[] a2 = (Object[]) o2;

			if (a1.length != a2.length) {
				return false;
			}

			for (int idx = 0; idx < a1.length; idx++) {
				if (!isEqual(a1[idx], a2[idx])) {
					return false;
				}
			}
			return true;
		}

		return Objects.equals(o1, o2);
	}

	private static void compareMethods(String contextPrefix, IBinaryMethod expectedMethod, IBinaryMethod actualMethod) {
		contextPrefix = contextPrefix + "." + safeString(expectedMethod.getSelector()); //$NON-NLS-1$
		compareAnnotations(contextPrefix, expectedMethod.getAnnotations(), actualMethod.getAnnotations());

		assertEquals(contextPrefix + ": The argument names didn't match.", expectedMethod.getArgumentNames(), //$NON-NLS-1$
				actualMethod.getArgumentNames());

		assertEquals(contextPrefix + ": The default values didn't match.", expectedMethod.getDefaultValue(), //$NON-NLS-1$
				actualMethod.getDefaultValue());

		assertEquals(contextPrefix + ": The exception type names did not match.", //$NON-NLS-1$
				expectedMethod.getExceptionTypeNames(), actualMethod.getExceptionTypeNames());

		compareGenericSignatures(contextPrefix + ": The method's generic signature did not match", //$NON-NLS-1$
				expectedMethod.getGenericSignature(), actualMethod.getGenericSignature());

		assertEquals(contextPrefix + ": The method descriptors did not match.", expectedMethod.getMethodDescriptor(), //$NON-NLS-1$
				actualMethod.getMethodDescriptor());
		assertEquals(contextPrefix + ": The modifiers didn't match.", expectedMethod.getModifiers(), //$NON-NLS-1$
				actualMethod.getModifiers());

		char[] classFileName = "".toCharArray(); //$NON-NLS-1$
		int minAnnotatedParameters = Math.min(expectedMethod.getAnnotatedParametersCount(),
				actualMethod.getAnnotatedParametersCount());
		for (int idx = 0; idx < minAnnotatedParameters; idx++) {
			compareAnnotations(contextPrefix, expectedMethod.getParameterAnnotations(idx, classFileName),
					actualMethod.getParameterAnnotations(idx, classFileName));
		}
		for (int idx = minAnnotatedParameters; idx < expectedMethod.getAnnotatedParametersCount(); idx++) {
			compareAnnotations(contextPrefix, new IBinaryAnnotation[0],
					expectedMethod.getParameterAnnotations(idx, classFileName));
		}
		for (int idx = minAnnotatedParameters; idx < actualMethod.getAnnotatedParametersCount(); idx++) {
			compareAnnotations(contextPrefix, new IBinaryAnnotation[0],
					actualMethod.getParameterAnnotations(idx, classFileName));
		}

		assertEquals(contextPrefix + ": The selectors did not match", expectedMethod.getSelector(), //$NON-NLS-1$
				actualMethod.getSelector());
		assertEquals(contextPrefix + ": The tag bits did not match", expectedMethod.getTagBits(), //$NON-NLS-1$
				actualMethod.getTagBits());

		compareTypeAnnotations(contextPrefix, expectedMethod.getTypeAnnotations(), actualMethod.getTypeAnnotations());
	}

	/**
	 * The index always provides complete generic signatures whereas some or all of the generic signature is optional
	 * for class files, so the generic signatures are expected to differ in certain situations.
	 */
	private static void compareGenericSignatures(String message, char[] expected, char[] actual) {
		assertEquals(message, expected, actual);
	}

	private static void compareTypeAnnotations(String contextPrefix, IBinaryTypeAnnotation[] expectedTypeAnnotations,
			IBinaryTypeAnnotation[] actualTypeAnnotations) {
		if (expectedTypeAnnotations == null) {
			if (actualTypeAnnotations != null) {
				throw new IllegalStateException(contextPrefix + ": Expected null for the annotation list but found: " //$NON-NLS-1$
						+ actualTypeAnnotations.toString());
			}
			return;
		}

		assertEquals(contextPrefix + ": The expected and actual number of type annotations did not match", //$NON-NLS-1$
				expectedTypeAnnotations.length, actualTypeAnnotations.length);

		for (int idx = 0; idx < expectedTypeAnnotations.length; idx++) {
			assertEquals(contextPrefix + ": Type annotation number " + idx + " did not match", //$NON-NLS-1$//$NON-NLS-2$
					expectedTypeAnnotations[idx], actualTypeAnnotations[idx]);
		}
	}

	private static void compareAnnotations(String contextPrefix, IBinaryAnnotation[] expectedBinaryAnnotations,
			IBinaryAnnotation[] actualBinaryAnnotations) {
		if (expectedBinaryAnnotations == null || expectedBinaryAnnotations.length == 0) {
			if (actualBinaryAnnotations != null && actualBinaryAnnotations.length != 0) {
				throw new IllegalStateException(contextPrefix + ": Expected null for the binary annotations"); //$NON-NLS-1$
			} else {
				return;
			}
		}
		if (actualBinaryAnnotations == null) {
			throw new IllegalStateException(contextPrefix + ": Actual null for the binary annotations"); //$NON-NLS-1$
		}
		if (expectedBinaryAnnotations.length != actualBinaryAnnotations.length) {
			throw new IllegalStateException(
					contextPrefix + ": The expected and actual number of annotations differed. Expected: " //$NON-NLS-1$
							+ expectedBinaryAnnotations.length + ", actual: " + actualBinaryAnnotations.length); //$NON-NLS-1$
		}

		for (int idx = 0; idx < expectedBinaryAnnotations.length; idx++) {
			if (!isEqual(expectedBinaryAnnotations[idx], actualBinaryAnnotations[idx])) {
				throw new IllegalStateException(contextPrefix + ": An annotation had an unexpected value"); //$NON-NLS-1$
			}
		}
	}

	private static void compareFields(String contextPrefix, IBinaryField field1, IBinaryField field2) {
		contextPrefix = contextPrefix + "." + safeString(field1.getName()); //$NON-NLS-1$
		compareAnnotations(contextPrefix, field1.getAnnotations(), field2.getAnnotations());
		assertEquals(contextPrefix + ": Constants not equal", field1.getConstant(), field2.getConstant()); //$NON-NLS-1$
		compareGenericSignatures(contextPrefix + ": The generic signature did not match", field1.getGenericSignature(), //$NON-NLS-1$
				field2.getGenericSignature());
		assertEquals(contextPrefix + ": The modifiers did not match", field1.getModifiers(), field2.getModifiers()); //$NON-NLS-1$
		assertEquals(contextPrefix + ": The tag bits did not match", field1.getTagBits(), field2.getTagBits()); //$NON-NLS-1$
		assertEquals(contextPrefix + ": The names did not match", field1.getName(), field2.getName()); //$NON-NLS-1$

		compareTypeAnnotations(contextPrefix, field1.getTypeAnnotations(), field2.getTypeAnnotations());
		assertEquals(contextPrefix + ": The type names did not match", field1.getTypeName(), field2.getTypeName()); //$NON-NLS-1$
	}

	private static String safeString(char[] name) {
		if (name == null) {
			return "<unnamed>"; //$NON-NLS-1$
		}
		return new String(name);
	}

}
