/* *******************************************************************
 * Copyright (c) 2002 Palo Alto Research Center, Incorporated (PARC).
 * 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 
 * ******************************************************************/

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

import java.lang.reflect.Modifier;

import org.aspectj.ajdt.internal.compiler.lookup.EclipseFactory;
import org.aspectj.ajdt.internal.compiler.lookup.EclipseTypeMunger;
import org.aspectj.ajdt.internal.compiler.problem.AjProblemReporter;
import org.aspectj.org.eclipse.jdt.internal.compiler.ClassFile;
import org.aspectj.org.eclipse.jdt.internal.compiler.CompilationResult;
import org.aspectj.org.eclipse.jdt.internal.compiler.ast.Argument;
import org.aspectj.org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
import org.aspectj.org.eclipse.jdt.internal.compiler.ast.LocalDeclaration;
import org.aspectj.org.eclipse.jdt.internal.compiler.ast.TypeParameter;
import org.aspectj.org.eclipse.jdt.internal.compiler.ast.TypeReference;
import org.aspectj.org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
import org.aspectj.org.eclipse.jdt.internal.compiler.codegen.CodeStream;
import org.aspectj.org.eclipse.jdt.internal.compiler.codegen.Opcodes;
import org.aspectj.org.eclipse.jdt.internal.compiler.flow.FlowContext;
import org.aspectj.org.eclipse.jdt.internal.compiler.flow.FlowInfo;
import org.aspectj.org.eclipse.jdt.internal.compiler.flow.InitializationFlowContext;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.ClassScope;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.ExtraCompilerModifiers;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.LocalVariableBinding;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.MethodBinding;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SourceTypeBinding;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.TagBits;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.TypeBinding;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.TypeVariableBinding;
import org.aspectj.org.eclipse.jdt.internal.compiler.parser.Parser;
import org.aspectj.org.eclipse.jdt.internal.compiler.problem.AbortCompilationUnit;
import org.aspectj.weaver.AjAttribute;
import org.aspectj.weaver.AjcMemberMaker;
import org.aspectj.weaver.Constants;
import org.aspectj.weaver.NameMangler;
import org.aspectj.weaver.NewMethodTypeMunger;
import org.aspectj.weaver.ResolvedMember;
import org.aspectj.weaver.ResolvedMemberImpl;
import org.aspectj.weaver.ResolvedType;
import org.aspectj.weaver.Shadow;
import org.aspectj.weaver.UnresolvedType;

/**
 * An inter-type method declaration.
 * 
 * @author Jim Hugunin
 */
public class InterTypeMethodDeclaration extends InterTypeDeclaration {
	public InterTypeMethodDeclaration(CompilationResult result, TypeReference onType) {
		super(result, onType);
	}

	public void parseStatements(Parser parser, CompilationUnitDeclaration unit) {
		if (ignoreFurtherInvestigation)
			return;
		if (!Modifier.isAbstract(declaredModifiers)) {
			parser.parse(this, unit);
		}
	}

	protected char[] getPrefix() {
		return (NameMangler.ITD_PREFIX + "interMethod$").toCharArray();
	}

	public boolean isFinal() {
		return (declaredModifiers & ClassFileConstants.AccFinal) != 0;
	}

//	public boolean isAbstract() {
//		boolean b = (declaredModifiers & ClassFileConstants.AccAbstract) != 0;
//		return b;//super.isAbstract();
//	}

	@Override
	public void analyseCode(ClassScope classScope, FlowContext flowContext, FlowInfo flowInfo) {
		if (Modifier.isAbstract(declaredModifiers))
			return;

		super.analyseCode(classScope, flowContext, flowInfo);
	}

	public void resolve(ClassScope upperScope) {
		if (munger == null)
			ignoreFurtherInvestigation = true;
		if (binding == null)
			ignoreFurtherInvestigation = true;
		if (ignoreFurtherInvestigation)
			return;

		if (!Modifier.isStatic(declaredModifiers)) {
			this.arguments = AstUtil.insert(AstUtil.makeFinalArgument("ajc$this_".toCharArray(), onTypeBinding), this.arguments);
			binding.parameters = AstUtil.insert(onTypeBinding, binding.parameters);
			
			// If the inserted argument is a generic type, we should include the associated type variables to ensure
			// the generated signature is correct (it will be checked by eclipse when this type is consumed in binary form).
			TypeVariableBinding onTypeTVBs[] = onTypeBinding.typeVariables();
			if (onTypeTVBs!=null && onTypeTVBs.length!=0) {
				// The type parameters don't seem to need to be correct
	//			TypeParameter tp = new TypeParameter();
	//			tp.binding = tvb[0];
	//			tp.name = tvb[0].sourceName;
	//			this.typeParameters = AstUtil.insert(tp,this.typeParameters);
				binding.typeVariables = AstUtil.insert(onTypeBinding.typeVariables(), binding.typeVariables);
			}
		}

		super.resolve(upperScope);
	}

	public void resolveStatements() {
		checkAndSetModifiersForMethod();
		if ((modifiers & ExtraCompilerModifiers.AccSemicolonBody) != 0) {
			if ((declaredModifiers & ClassFileConstants.AccAbstract) == 0)
				scope.problemReporter().methodNeedBody(this);
		} else {
			// the method HAS a body --> abstract native modifiers are forbiden
			if (((declaredModifiers & ClassFileConstants.AccAbstract) != 0))
				scope.problemReporter().methodNeedingNoBody(this);
		}

		// XXX AMC we need to do this, but I'm not 100% comfortable as I don't
		// know why the return type is wrong in this case. Also, we don't seem to need
		// to do it for args...
		if (munger.getSignature().getReturnType().isRawType()) {
			if (!binding.returnType.isRawType()) {
				EclipseFactory world = EclipseFactory.fromScopeLookupEnvironment(scope);
				binding.returnType = world.makeTypeBinding(munger.getSignature().getReturnType());
			}
		}

		// check @Override annotation - based on MethodDeclaration.resolveStatements() @Override processing
		checkOverride: {
			if (this.binding == null)
				break checkOverride;
			if (this.scope.compilerOptions().sourceLevel < ClassFileConstants.JDK1_5)
				break checkOverride;
			boolean hasOverrideAnnotation = (this.binding.tagBits & TagBits.AnnotationOverride) != 0;

			// Need to verify
			if (hasOverrideAnnotation) {

				// Work out the real method binding that we can use for comparison
				EclipseFactory world = EclipseFactory.fromScopeLookupEnvironment(scope);
				MethodBinding realthing = world.makeMethodBinding(munger.getSignature(), munger.getTypeVariableAliases());

				boolean reportError = true;
				// Go up the hierarchy, looking for something we override
				ReferenceBinding supertype = onTypeBinding.superclass();
				while (supertype != null && reportError) {
					MethodBinding[] possibles = supertype.getMethods(declaredSelector);
					for (int i = 0; i < possibles.length; i++) {
						MethodBinding mb = possibles[i];

						boolean couldBeMatch = true;
						if (mb.parameters.length != realthing.parameters.length)
							couldBeMatch = false;
						else {
							for (int j = 0; j < mb.parameters.length && couldBeMatch; j++) {
								if (!mb.parameters[j].equals(realthing.parameters[j]))
									couldBeMatch = false;
							}
						}
						// return types compatible? (allow for covariance)
						if (couldBeMatch && !returnType.resolvedType.isCompatibleWith(mb.returnType))
							couldBeMatch = false;
						if (couldBeMatch)
							reportError = false;
					}
					supertype = supertype.superclass(); // superclass of object is null
				}
				// If we couldn't find something we override, report the error
				if (reportError)
					((AjProblemReporter) this.scope.problemReporter()).itdMethodMustOverride(this, realthing);
			}
		}

		if (!Modifier.isAbstract(declaredModifiers))
			super.resolveStatements();
		if (Modifier.isStatic(declaredModifiers)) {
			// Check the target for ITD is not an interface
			if (onTypeBinding.isInterface()) {
				scope.problemReporter().signalError(sourceStart, sourceEnd, "methods in interfaces cannot be declared static");
			}
		}
	}

	public EclipseTypeMunger build(ClassScope classScope) {
		EclipseFactory factory = EclipseFactory.fromScopeLookupEnvironment(classScope);

		resolveOnType(classScope);
		if (ignoreFurtherInvestigation)
			return null;

		binding = classScope.referenceContext.binding.resolveTypesFor(binding);
		if (binding == null) {
			// if binding is null, we failed to find a type used in the method params, this error
			// has already been reported.
			this.ignoreFurtherInvestigation = true;
			// return null;
			throw new AbortCompilationUnit(compilationResult, null);
		}

		if (isTargetAnnotation(classScope, "method"))
			return null; // Error message output in isTargetAnnotation
		if (isTargetEnum(classScope, "method"))
			return null; // Error message output in isTargetEnum

		if (interTypeScope == null)
			return null; // We encountered a problem building the scope, don't continue - error already reported

		// This signature represents what we want consumers of the targetted type to 'see'
		// must use the factory method to build it since there may be typevariables from the binding
		// referred to in the parameters/returntype
		ResolvedMemberImpl sig = factory.makeResolvedMemberForITD(binding, onTypeBinding, interTypeScope.getRecoveryAliases());
		sig.resetName(new String(declaredSelector));
		int resetModifiers = declaredModifiers;
		if (binding.isVarargs())
			resetModifiers = resetModifiers | Constants.ACC_VARARGS;
		sig.resetModifiers(resetModifiers);
		NewMethodTypeMunger myMunger = new NewMethodTypeMunger(sig, null, typeVariableAliases);
		setMunger(myMunger);
		ResolvedType aspectType = factory.fromEclipse(classScope.referenceContext.binding);
		ResolvedMember me = myMunger.getInterMethodBody(aspectType);
		this.selector = binding.selector = me.getName().toCharArray();
		return new EclipseTypeMunger(factory, myMunger, aspectType, this);
	}

	private AjAttribute makeAttribute() {
		return new AjAttribute.TypeMunger(munger);
	}

	public void generateCode(ClassScope classScope, ClassFile classFile) {
		if (ignoreFurtherInvestigation) {
			// System.err.println("no code for " + this);
			return;
		}

		classFile.extraAttributes.add(new EclipseAttributeAdapter(makeAttribute()));

		if (!Modifier.isAbstract(declaredModifiers)) {
			super.generateCode(classScope, classFile); // this makes the interMethodBody
		}

		// annotations on the ITD declaration get put on this method
		generateDispatchMethod(classScope, classFile);
	}

	public void generateDispatchMethod(ClassScope classScope, ClassFile classFile) {
		EclipseFactory world = EclipseFactory.fromScopeLookupEnvironment(classScope);

		UnresolvedType aspectType = world.fromBinding(classScope.referenceContext.binding);
		ResolvedMember signature = munger.getSignature();

		ResolvedMember dispatchMember = AjcMemberMaker.interMethodDispatcher(signature, aspectType);
		MethodBinding dispatchBinding = world.makeMethodBinding(dispatchMember, munger.getTypeVariableAliases(), munger
				.getSignature().getDeclaringType());
		MethodBinding introducedMethod = world.makeMethodBinding(AjcMemberMaker.interMethod(signature, aspectType, onTypeBinding
				.isInterface()), munger.getTypeVariableAliases());

		classFile.generateMethodInfoHeader(dispatchBinding);
		int methodAttributeOffset = classFile.contentsOffset;

		// Watch out! We are passing in 'binding' here (instead of dispatchBinding) so that
		// the dispatch binding attributes will include the annotations from the 'binding'.
		// There is a chance that something else on the binding (e.g. throws clause) might
		// damage the attributes generated for the dispatch binding.
		int attributeNumber = classFile.generateMethodInfoAttributes(binding, makeEffectiveSignatureAttribute(signature,
				Shadow.MethodCall, false));
		int codeAttributeOffset = classFile.contentsOffset;
		classFile.generateCodeAttributeHeader();
		CodeStream codeStream = classFile.codeStream;
		codeStream.reset(this, classFile);
		codeStream.initializeMaxLocals(dispatchBinding);

		Argument[] itdArgs = this.arguments;
		if (itdArgs != null) {
			for (int a = 0; a < itdArgs.length; a++) {
				LocalVariableBinding lvb = itdArgs[a].binding;
				LocalVariableBinding lvbCopy = new LocalVariableBinding(lvb.name, lvb.type, lvb.modifiers, true);
				// e37: have to create a declaration so that the check in ClassFile (line 2538) won't skip it
				lvbCopy.declaration = new LocalDeclaration(itdArgs[a].name,0,0);
				codeStream.record(lvbCopy);
				lvbCopy.recordInitializationStartPC(0);
				lvbCopy.resolvedPosition = lvb.resolvedPosition;
			}
		}

		MethodBinding methodBinding = introducedMethod;
		TypeBinding[] parameters = methodBinding.parameters;
		int length = parameters.length;
		int resolvedPosition;
		if (methodBinding.isStatic())
			resolvedPosition = 0;
		else {
			codeStream.aload_0();
			resolvedPosition = 1;
		}
		for (int i = 0; i < length; i++) {
			codeStream.load(parameters[i], resolvedPosition);
			if ((parameters[i] == TypeBinding.DOUBLE) || (parameters[i] == TypeBinding.LONG))
				resolvedPosition += 2;
			else
				resolvedPosition++;
		}
		// TypeBinding type;
		if (methodBinding.isStatic())
			codeStream.invoke(Opcodes.OPC_invokestatic,methodBinding,null);
		else {
			if (methodBinding.declaringClass.isInterface()) {
				codeStream.invoke(Opcodes.OPC_invokeinterface, methodBinding, null);
			} else {
				codeStream.invoke(Opcodes.OPC_invokevirtual, methodBinding, null);
			}
		}
		AstUtil.generateReturn(dispatchBinding.returnType, codeStream);

		// tag the local variables as used throughout the method
		if (itdArgs != null && codeStream.locals != null) {
			for (int a = 0; a < itdArgs.length; a++) {
				if (codeStream.locals[a] != null) {
					codeStream.locals[a].recordInitializationEndPC(codeStream.position);
				}
			}
		}
		classFile.completeCodeAttribute(codeAttributeOffset);
		attributeNumber++;
		classFile.completeMethodInfo(binding,methodAttributeOffset, attributeNumber);
	}

	protected Shadow.Kind getShadowKindForBody() {
		return Shadow.MethodExecution;
	}

	// XXX this code is copied from MethodScope, with a few adjustments for ITDs...
	private void checkAndSetModifiersForMethod() {

		// for reported problems, we want the user to see the declared selector
		char[] realSelector = this.selector;
		this.selector = declaredSelector;

		final ReferenceBinding declaringClass = this.binding.declaringClass;
		if ((declaredModifiers & ExtraCompilerModifiers.AccAlternateModifierProblem) != 0)
			scope.problemReporter().duplicateModifierForMethod(onTypeBinding, this);

		// after this point, tests on the 16 bits reserved.
		int realModifiers = declaredModifiers & ExtraCompilerModifiers.AccJustFlag;

		// check for abnormal modifiers
		int unexpectedModifiers = ~(ClassFileConstants.AccPublic | ClassFileConstants.AccPrivate | ClassFileConstants.AccProtected
				| ClassFileConstants.AccAbstract | ClassFileConstants.AccStatic | ClassFileConstants.AccFinal
				| ClassFileConstants.AccSynchronized | ClassFileConstants.AccNative | ClassFileConstants.AccStrictfp);
		if ((realModifiers & unexpectedModifiers) != 0) {
			scope.problemReporter().illegalModifierForMethod(this);
			declaredModifiers &= ~ExtraCompilerModifiers.AccJustFlag | ~unexpectedModifiers;
		}

		// check for incompatible modifiers in the visibility bits, isolate the visibility bits
		int accessorBits = realModifiers
				& (ClassFileConstants.AccPublic | ClassFileConstants.AccProtected | ClassFileConstants.AccPrivate);
		if ((accessorBits & (accessorBits - 1)) != 0) {
			scope.problemReporter().illegalVisibilityModifierCombinationForMethod(onTypeBinding, this);

			// need to keep the less restrictive so disable Protected/Private as necessary
			if ((accessorBits & ClassFileConstants.AccPublic) != 0) {
				if ((accessorBits & ClassFileConstants.AccProtected) != 0)
					declaredModifiers &= ~ClassFileConstants.AccProtected;
				if ((accessorBits & ClassFileConstants.AccPrivate) != 0)
					declaredModifiers &= ~ClassFileConstants.AccPrivate;
			} else if ((accessorBits & ClassFileConstants.AccProtected) != 0 && (accessorBits & ClassFileConstants.AccPrivate) != 0) {
				declaredModifiers &= ~ClassFileConstants.AccPrivate;
			}
		}

		// check for modifiers incompatible with abstract modifier
		if ((declaredModifiers & ClassFileConstants.AccAbstract) != 0) {
			int incompatibleWithAbstract = ClassFileConstants.AccStatic | ClassFileConstants.AccFinal
					| ClassFileConstants.AccSynchronized | ClassFileConstants.AccNative | ClassFileConstants.AccStrictfp;
			if ((declaredModifiers & incompatibleWithAbstract) != 0)
				scope.problemReporter().illegalAbstractModifierCombinationForMethod(onTypeBinding, this);
			if (!onTypeBinding.isAbstract())
				scope.problemReporter().abstractMethodInAbstractClass((SourceTypeBinding) onTypeBinding, this);
		}

		/*
		 * DISABLED for backward compatibility with javac (if enabled should also mark private methods as final) // methods from a
		 * final class are final : 8.4.3.3 if (methodBinding.declaringClass.isFinal()) modifiers |= AccFinal;
		 */
		// native methods cannot also be tagged as strictfp
		if ((declaredModifiers & ClassFileConstants.AccNative) != 0 && (declaredModifiers & ClassFileConstants.AccStrictfp) != 0)
			scope.problemReporter().nativeMethodsCannotBeStrictfp(onTypeBinding, this);

		// static members are only authorized in a static member or top level type
		if (((realModifiers & ClassFileConstants.AccStatic) != 0) && declaringClass.isNestedType() && !declaringClass.isStatic())
			scope.problemReporter().unexpectedStaticModifierForMethod(onTypeBinding, this);

		// restore the true selector now that any problems have been reported
		this.selector = realSelector;
	}
}
