/* *******************************************************************
 * Copyright (c) 2002,2005 Contributors.
 * All rights reserved. 
 * This program and the accompanying materials are made available 
 * under the terms of the Eclipse Public License v1.0 
 * which accompanies this distribution and is available at 
 * http://www.eclipse.org/legal/epl-v10.html 
 *  
 * Contributors: 
 *     PARC     initial implementation 
 *     Andy Clement - upgrade to support fields targetting generic types
 * ******************************************************************/

package org.aspectj.ajdt.internal.compiler.lookup;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.aspectj.org.eclipse.jdt.core.compiler.CharOperation;
import org.aspectj.org.eclipse.jdt.core.compiler.IProblem;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.Binding;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.CompilationUnitScope;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.FieldBinding;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.IMemberFinder;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.InvocationSite;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.MethodBinding;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.ProblemFieldBinding;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.Scope;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SourceTypeBinding;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.TypeBinding;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.TypeVariableBinding;

/**
 * The member finder looks after intertype declared members on a type, there is one member finder per type that was hit by an ITD.
 */
public class InterTypeMemberFinder implements IMemberFinder {
	private List<FieldBinding> interTypeFields = new ArrayList<FieldBinding>();
	private List<MethodBinding> interTypeMethods = new ArrayList<MethodBinding>();

	public SourceTypeBinding sourceTypeBinding;

	@Override
	public FieldBinding getField(SourceTypeBinding sourceTypeBinding, char[] fieldName, InvocationSite site, Scope scope) {
		FieldBinding retField = sourceTypeBinding.getFieldBase(fieldName, true); // XXX may need to get the correct value for second
		// parameter in the future (see #55341)
		if (interTypeFields.isEmpty()) {
			return retField;
		}
		int fieldLength = fieldName.length;

		for (int i = 0, len = interTypeFields.size(); i < len; i++) {
			FieldBinding field = interTypeFields.get(i);
			if (field.name.length == fieldLength && CharOperation.prefixEquals(field.name, fieldName)) {
				retField = resolveConflicts(sourceTypeBinding, retField, field, site, scope);
			}
		}

		return retField;
	}

	private FieldBinding resolveConflicts(SourceTypeBinding sourceTypeBinding, FieldBinding retField, FieldBinding field,
			InvocationSite site, Scope scope) {
		if (retField == null) {
			return field;
		}
		if (site != null) {
			if (!field.canBeSeenBy(sourceTypeBinding, site, scope)) {
				return retField;
			}
			if (!retField.canBeSeenBy(sourceTypeBinding, site, scope)) {
				return field;
			}
		}
		// XXX need dominates check on aspects
		return new ProblemFieldBinding(retField.declaringClass, retField.name, IProblem.AmbiguousField);// ProblemReporter.Ambiguous);
	}

	// private void reportConflicts(SourceTypeBinding sourceTypeBinding,
	// MethodBinding m1, MethodBinding m2)
	// {
	// if (m1 == m2) {
	// System.err.println("odd that we're ecomparing the same: " + m1);
	// return;
	// }
	//
	// if (!m1.areParametersEqual(m2)) return;

	// if (m1 instanceof InterTypeMethodBinding) {
	// if (m2 instanceof InterTypeMethodBinding) {
	// reportConflictsBoth(sourceTypeBinding,
	// (InterTypeMethodBinding)m1,
	// (InterTypeMethodBinding)m2);
	// } else {
	// reportConflictsOne(sourceTypeBinding,
	// (InterTypeMethodBinding)m1,
	// m2);
	// }
	// } else if (m2 instanceof InterTypeMethodBinding) {
	// reportConflictsOne(sourceTypeBinding,
	// (InterTypeMethodBinding)m2,
	// m1);
	// } else {
	// reportConflictsNone(sourceTypeBinding,
	// m2,
	// m1);
	// }
	// }

	// private void reportConflicts(
	// SourceTypeBinding sourceTypeBinding,
	// MethodBinding m1,
	// MethodBinding m2)
	// {
	// //System.err.println("compare: " + m1 + " with " + m2);
	//
	// if (m1 == m2) {
	// System.err.println("odd that we're ecomparing the same: " + m1);
	// return;
	// }
	//
	// if (!m1.areParametersEqual(m2)) return;
	//
	// //System.err.println("t1: " + getTargetType(m1) + ", " + getTargetType(m2));
	//
	// if (getTargetType(m1) != getTargetType(m2)) return;
	//
	// if (m1.declaringClass == m2.declaringClass) {
	// duplicateMethodBinding(m1, m2);
	// return;
	// }
	//
	//
	// if (m1.isPublic() || m2.isPublic()) {
	// duplicateMethodBinding(m1, m2);
	// return;
	// }
	//
	// // handle the wierd case where the aspect is a subtype of the target
	// if (m2.isProtected()) {
	// if (m2.declaringClass.isSuperclassOf(m1.declaringClass)) {
	// duplicateMethodBinding(m1, m2);
	// }
	// // don't return because we also want to do the package test
	// }
	//
	// if (!m1.isPrivate() || !m2.isPrivate()) {
	// // at least package visible
	// if (m1.declaringClass.getPackage() == m2.declaringClass.getPackage()) {
	// duplicateMethodBinding(m1, m2);
	// }
	// return;
	// }
	//
	// //XXX think about inner types some day
	// }
	// //
	private boolean isVisible(MethodBinding m1, ReferenceBinding s) {
		if (m1.declaringClass == s) {
			return true;
		}

		if (m1.isPublic()) {
			return true;
		}

		// don't need to handle protected
		// if (m1.isProtected()) {

		if (!m1.isPrivate()) {
			// at least package visible
			return (m1.declaringClass.getPackage() == s.getPackage());
		}

		return false;
	}

	//
	// private void duplicateMethodBinding(MethodBinding m1, MethodBinding m2) {
	// ReferenceBinding t1 = m1.declaringClass;
	// ReferenceBinding t2 = m2.declaringClass;
	//
	//
	//
	//
	//
	//
	// if (!(t1 instanceof SourceTypeBinding) || !(t2 instanceof SourceTypeBinding)) {
	// throw new RuntimeException("unimplemented");
	// }
	//
	// SourceTypeBinding s1 = (SourceTypeBinding)t1;
	// SourceTypeBinding s2 = (SourceTypeBinding)t2;
	//
	// if (m1.sourceMethod() != null) {
	// s1.scope.problemReporter().duplicateMethodInType(s2, m1.sourceMethod());
	// }
	// if (m2.sourceMethod() != null) {
	// s2.scope.problemReporter().duplicateMethodInType(s1, m2.sourceMethod());
	// }
	// }

	// private void reportConflictsNone(
	// SourceTypeBinding sourceTypeBinding,
	// MethodBinding m2,
	// MethodBinding m1)
	// {
	// throw new RuntimeException("not possible");
	// }

	// ReferenceBinding t1 = getDeclaringClass(m1);
	// //.declaringClass;
	// ReferenceBinding t2 = getDeclaringClass(m2);
	// //.declaringClass;
	//
	// if (t1 == t2) {
	// AbstractMethodDeclaration methodDecl = m2.sourceMethod(); // cannot be retrieved after binding is lost
	// System.err.println("duplicate: " + t1 + ", " + t2);
	// sourceTypeBinding.scope.problemReporter().duplicateMethodInType(sourceTypeBinding, methodDecl);
	// methodDecl.binding = null;
	// //methods[m] = null; //XXX duplicate problem reports
	// return;
	// }
	//
	// if (!(t1 instanceof SourceTypeBinding) || !(t2 instanceof SourceTypeBinding)) {
	// throw new RuntimeException("unimplemented");
	// }
	//
	// SourceTypeBinding s1 = (SourceTypeBinding)t1;
	// SourceTypeBinding s2 = (SourceTypeBinding)t2;
	//
	//
	// if (m1.canBeSeenBy(s1, null, s2.scope) || m2.canBeSeenBy(s2, null, s1.scope)) {
	// s1.scope.problemReporter().duplicateMethodInType(s2, m1.sourceMethod());
	// s2.scope.problemReporter().duplicateMethodInType(s1, m2.sourceMethod());
	// }
	// }

	// private ReferenceBinding getTargetType(MethodBinding m2) {
	// if (m2 instanceof InterTypeMethodBinding) {
	// return ((InterTypeMethodBinding)m2).getTargetType();
	// }
	//
	// return m2.declaringClass;
	// }

	// find all of my methods, including ITDs
	// PLUS: any public ITDs made on interfaces that I implement
	@Override
	public MethodBinding[] methods(SourceTypeBinding sourceTypeBinding) {
		MethodBinding[] orig = sourceTypeBinding.methodsBase();
		// if (interTypeMethods.isEmpty()) return orig;

		List<MethodBinding> ret = new ArrayList<MethodBinding>(Arrays.asList(orig));
		for (int i = 0, len = interTypeMethods.size(); i < len; i++) {
			MethodBinding method = interTypeMethods.get(i);
			ret.add(method);
		}

		ReferenceBinding[] interfaces = sourceTypeBinding.superInterfaces();
		for (int i = 0; i < interfaces.length; i++) {
			if (interfaces[i] instanceof SourceTypeBinding) {
				SourceTypeBinding intSTB = (SourceTypeBinding) interfaces[i];
				addPublicITDSFrom(intSTB, ret);
			}
		}

		if (ret.isEmpty()) {
			return Binding.NO_METHODS;
		}
		return ret.toArray(new MethodBinding[ret.size()]);
	}

	private void addPublicITDSFrom(SourceTypeBinding anInterface, List<MethodBinding> accumulator) {
		if (anInterface.memberFinder != null) {
			InterTypeMemberFinder finder = (InterTypeMemberFinder) anInterface.memberFinder;
			for (MethodBinding aBinding: finder.interTypeMethods) {
				if (Modifier.isPublic(aBinding.modifiers)) {
					accumulator.add(aBinding);
				}
			}
		}
		ReferenceBinding superType = anInterface.superclass;
		if (superType instanceof SourceTypeBinding && superType.isInterface()) {
			addPublicITDSFrom((SourceTypeBinding) superType, accumulator);
		}
	}

	// XXX conflicts
	@Override
	public MethodBinding[] getMethods(SourceTypeBinding sourceTypeBinding, char[] selector) {
		// System.err.println("getMethods: " + new String(sourceTypeBinding.signature()) +
		// ", " + new String(selector));

		MethodBinding[] orig = sourceTypeBinding.getMethodsBase(selector);
		if (interTypeMethods.isEmpty()) {
			return orig;
		}

		Set<MethodBinding> ret = new HashSet<MethodBinding>(Arrays.asList(orig));
		// System.err.println("declared method: " + ret + " inters = " + interTypeMethods);

		for (int i = 0, len = interTypeMethods.size(); i < len; i++) {
			MethodBinding method = interTypeMethods.get(i);

			if (CharOperation.equals(selector, method.selector)) {
				ret.add(method);
			}
		}

		if (ret.isEmpty()) {
			return Binding.NO_METHODS;
		}

		// System.err.println("method: " + ret);

		// check for conflicts
		// int len = ret.size();
		// if (len > 1) {
		// for (int i=0; i <len; i++) {
		// MethodBinding m1 = (MethodBinding)ret.get(i);
		// for (int j=i+1; j < len; j++) {
		// MethodBinding m2 = (MethodBinding)ret.get(j);
		// //reportConflicts(sourceTypeBinding, m1, m2);
		// }
		// }
		// }

		// System.err.println("got methods: " + ret + " on " + sourceTypeBinding);

		return ret.toArray(new MethodBinding[ret.size()]);
	}

	@Override
	public MethodBinding getExactMethod(SourceTypeBinding sourceTypeBinding, char[] selector, TypeBinding[] argumentTypes,
			CompilationUnitScope refScope) {
		MethodBinding ret = sourceTypeBinding.getExactMethodBase(selector, argumentTypes, refScope);

		// An intertype declaration may override an inherited member (Bug#50776)
		for (int i = 0, len = interTypeMethods.size(); i < len; i++) {
			MethodBinding im = interTypeMethods.get(i);
			if (matches(im, selector, argumentTypes)) {
				return im;
			}
		}
		return ret;
	}

	// if (isVisible(im, sourceTypeBinding)) {
	// if (ret == null) {
	// ret = im;
	// } else {
	// ret = resolveOverride(ret, im);
	// }
	// }
	// }
	// }
	// return ret;
	// }

	// private MethodBinding resolveOverride(MethodBinding m1, MethodBinding m2) {
	// ReferenceBinding t1 = getTargetType(m1);
	// ReferenceBinding t2 = getTargetType(m2);
	// if (t1 == t2) {
	// //XXX also need a test for completely matching sigs
	// if (m1.isAbstract()) return m2;
	// else if (m2.isAbstract()) return m1;
	//
	//
	// if (m1 instanceof InterTypeMethodBinding) {
	// //XXX need to handle dominates here
	// EclipseWorld world = EclipseWorld.fromScopeLookupEnvironment(sourceTypeBinding.scope);
	// int cmp = compareAspectPrecedence(world.fromEclipse(m1.declaringClass),
	// world.fromEclipse(m2.declaringClass));
	// if (cmp < 0) return m2;
	// else if (cmp > 0) return m1;
	// }
	//
	// duplicateMethodBinding(m1, m2);
	// return null;
	// }
	// if (t1.isSuperclassOf(t2)) {
	// return m2;
	// }
	// if (t2.isSuperclassOf(t1)) {
	// return m1;
	// }
	//
	// duplicateMethodBinding(m1, m2);
	// return null;
	// }

	// private int compareAspectPrecedence(ResolvedType a1, ResolvedType a2) {
	// World world = a1.getWorld();
	// int ret = world.compareByDominates(a1, a2);
	// if (ret == 0) {
	// if (a1.isAssignableFrom(a2)) return -1;
	// if (a2.isAssignableFrom(a1)) return +1;
	// }
	// return ret;
	// }

	//
	public static boolean matches(MethodBinding m1, MethodBinding m2) {
		return matches(m1, m2.selector, m2.parameters);
		// &&
		// (isVisible(m1, m2.declaringClass) || isVisible(m2, m1.declaringClass));
	}

	public static boolean matches2(MethodBinding newBinding, MethodBinding existingBinding) {
		if (!CharOperation.equals(existingBinding.selector, newBinding.selector)) {
			return false;
		}
		int argCount = existingBinding.parameters.length;
		if (newBinding.parameters.length != argCount) {
			return false;
		}
		TypeBinding[] toMatch = newBinding.parameters;
		boolean matches = true;
		for (int p = 0; p < argCount; p++) {
			if (toMatch[p] != existingBinding.parameters[p]) {
				matches = false;
				break;
			}
		}
		if (!matches) {
			// could be due to generics differences. For example:
			// existingBinding is <T extends Foo> T bar(T t)
			// newBinding is <T extends Foo> T bar(T t)
			// the '==' check isn't going to pass, we need to do a deeper check
			TypeVariableBinding[] existingTVBs = existingBinding.typeVariables;
			TypeVariableBinding[] newTVBs = newBinding.typeVariables;
			if (existingTVBs.length != 0 && existingTVBs.length == newTVBs.length) {
				// Check them
				boolean genericsMatch = true;
				for (int t = 0, max = existingTVBs.length; t < max; t++) {
					char[] existingSig = existingTVBs[t].genericSignature();
					char[] newSig = newTVBs[t].genericSignature();
					if (!CharOperation.equals(existingSig, newSig)) {
						genericsMatch = false;
						break;
					}
				}
				if (genericsMatch) {
					for (int a = 0; a < argCount; a++) {
						char[] existingSig = existingBinding.parameters[a].genericTypeSignature();
						char[] newSig = toMatch[a].genericTypeSignature();
						if (!CharOperation.equals(existingSig, newSig)) {
							genericsMatch = false;
							break;
						}
					}
				}
				if (genericsMatch) {
					matches = true;
				}
			}
		}
		return matches;
	}

	private static boolean matches(MethodBinding method, char[] selector, TypeBinding[] argumentTypes) {
		if (!CharOperation.equals(selector, method.selector)) {
			return false;
		}
		int argCount = argumentTypes.length;
		if (method.parameters.length != argCount) {
			return false;
		}
		TypeBinding[] toMatch = method.parameters;
		for (int p = 0; p < argCount; p++) {
			if (toMatch[p] != argumentTypes[p]) {
				return false;
			}
		}
		return true;
	}

	public void addInterTypeField(FieldBinding binding) {
		// System.err.println("adding: " + binding + " to " + this);
		interTypeFields.add(binding);
	}

	public void addInterTypeMethod(MethodBinding binding) {
		// check for conflicts with existing methods, should really check type as well...
		// System.err.println("adding: " + binding + " to " + sourceTypeBinding);
		// pr284862 - this can leave a broken EclipseResolvedMember with an orphan method binding inside...ought to repair it
		if (isVisible(binding, sourceTypeBinding)) {
			MethodBinding[] baseMethods = sourceTypeBinding.methods;
			for (int i = 0, len = baseMethods.length; i < len; i++) {
				MethodBinding b = baseMethods[i];
				sourceTypeBinding.resolveTypesFor(b); // this will return fast if its already been done.
				if (matches2(binding, b)) {
					// this always means we should remove the existing method
					if (b.sourceMethod() != null) {
						b.sourceMethod().binding = null;
					}
					sourceTypeBinding.removeMethod(i);
					// System.err.println("    left: " + Arrays.asList(sourceTypeBinding.methods));
					break;
				}
			}
		}

		interTypeMethods.add(binding);
	}

	/**
	 * @param name the name of the field
	 * @return true if already know about an intertype field declaration for that field
	 */
	public boolean definesField(String name) {
		List<FieldBinding> l = interTypeFields;
		if (l.size() > 0) {
			char[] nameChars = name.toCharArray();
			for (int i = 0; i < l.size(); i++) {
				FieldBinding fieldBinding = interTypeFields.get(i);
				if (CharOperation.equals(fieldBinding.name, nameChars)) {
					return true;
				}
			}
		}
		return false;
	}

}
