/*******************************************************************************
 * Copyright (c) 2009, 2015 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:
 *     Mike Kucera (IBM) - Initial API and implementation
 *     Sergey Prigogin (Google)
 *******************************************************************************/
package org.eclipse.cdt.core.parser.tests;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import org.eclipse.cdt.core.dom.ast.IASTNode;
import org.eclipse.cdt.internal.core.dom.parser.ASTNode;
import org.junit.Assert;

import junit.framework.AssertionFailedError;

public class ASTComparer extends Assert {

	private static Set<String> methodsToIgnore = new HashSet<>(Arrays.asList(
			// Prevent infinite recursion
			"getParent", "getTranslationUnit", "getLastName",

			// Exponential complexity
			"getOperand2", // duplicates getInitOperand2()
			"getChildren", "getAllSegments", // duplicates getQualifier()

			// Can be different in copy
			"isFrozen", "getContainingFilename", "getOriginalNode",

			// These methods are problematic
			"getProblem",

			// Ignore preprocessor nodes
			"getMacroDefinitions", "getBuiltinMacroDefinitions", "getIncludeDirectives", "getAllPreprocessorStatements",
			"getMacroExpansions", "getPreprocessorProblems", "getComments",

			// Avoid name resolution
			"isDeclaration", "isDefinition", "isReference", "isAssociatedWithLastName", "getNestingLevel",
			"getImplicitNames", "getImplicitDestructorNames", "isLValue",

			// These methods can return a special constant value, such as
			// ICPPASTFunctionDecalarator.NOEXCEPT_DEFAULT, which does not
			// get cloned when the node is copied.
			"getNoexceptExpression"));

	public static void assertCopy(IASTNode node1, IASTNode node2) {
		try {
			assertCopy(node1, node2, 0);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	private static void assertCopy(IASTNode node1, IASTNode node2, int n) throws Exception {
		if (node1 == null && node2 == null)
			return;
		assertNotNull(node1);
		assertNotNull(node2);
		assertFalse(node1 == node2); // must be distinct copy

		Class klass1 = node1.getClass();
		Class klass2 = node2.getClass();
		assertTrue(klass1.equals(klass2)); // nodes must be the same concrete type
		//System.out.println(spaces(n) + klass1.getSimpleName());

		BeanInfo beanInfo = Introspector.getBeanInfo(klass1);

		for (PropertyDescriptor property : beanInfo.getPropertyDescriptors()) {
			Method getter = property.getReadMethod();
			if (getter == null)
				continue;
			if (methodsToIgnore.contains(getter.getName()))
				continue;

			if (getter.getAnnotation(Deprecated.class) != null)
				continue;

			try {
				Class returnType = getter.getReturnType();

				if (IASTNode.class.isAssignableFrom(returnType)) {
					//System.out.println(spaces(n) + "Testing1: " + getter.getName());
					IASTNode result1 = (IASTNode) getter.invoke(node1);
					IASTNode result2 = (IASTNode) getter.invoke(node2);
					assertCopy(result1, result2, n + 1); // members must be same
				} else if (returnType.isArray() && IASTNode.class.isAssignableFrom(returnType.getComponentType())) {
					//System.out.println(spaces(n) + "Testing2: " + getter.getName());
					IASTNode[] result1 = (IASTNode[]) getter.invoke(node1);
					IASTNode[] result2 = (IASTNode[]) getter.invoke(node2);
					if (result1 == null && result2 == null)
						continue;
					assertNotNull(result1);
					assertNotNull(result2);
					assertEquals(result1.length, result2.length);
					for (int i = 0; i < result1.length; i++)
						assertCopy(result1[i], result2[i], n + 1);
				} else if ((returnType.isPrimitive() || returnType.equals(String.class))
						&& !returnType.equals(Void.class)) {
					//System.out.println(spaces(n) + "Testing3: " + getter.getName());
					Object result1 = getter.invoke(node1);
					Object result2 = getter.invoke(node2);
					assertEquals(result1, result2);
				}
			} catch (AssertionFailedError e) {
				System.out.printf("Failure when calling %s.%s() @(%d,%d)\n", node1.getClass().getSimpleName(),
						getter.getName(), ((ASTNode) node1).getOffset(), ((ASTNode) node1).getLength());
				throw e;
			}
		}

	}

	//	private static String spaces(int n) {
	//		char[] spaces = new char[n*2];
	//		Arrays.fill(spaces, ' ');
	//		return new String(spaces);
	//	}
}
