/* *******************************************************************
 * 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 java.util.ArrayList;
import java.util.List;

import org.aspectj.ajdt.internal.compiler.lookup.AjTypeConstants;
import org.aspectj.ajdt.internal.compiler.lookup.EclipseFactory;
import org.aspectj.ajdt.internal.compiler.lookup.PrivilegedHandler;
import org.aspectj.bridge.context.CompilationAndWeavingContext;
import org.aspectj.bridge.context.ContextToken;
import org.aspectj.org.eclipse.jdt.core.Flags;
import org.aspectj.org.eclipse.jdt.core.compiler.CharOperation;
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.Annotation;
import org.aspectj.org.eclipse.jdt.internal.compiler.ast.Argument;
import org.aspectj.org.eclipse.jdt.internal.compiler.ast.QualifiedTypeReference;
import org.aspectj.org.eclipse.jdt.internal.compiler.ast.SingleTypeReference;
import org.aspectj.org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
import org.aspectj.org.eclipse.jdt.internal.compiler.ast.TypeReference;
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.lookup.ArrayBinding;
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.MethodBinding;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.TypeBinding;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.TypeConstants;
import org.aspectj.weaver.Advice;
import org.aspectj.weaver.AdviceKind;
import org.aspectj.weaver.AjAttribute;
import org.aspectj.weaver.NameMangler;
import org.aspectj.weaver.ResolvedMember;
import org.aspectj.weaver.UnresolvedType;

/**
 * Represents before, after and around advice in an aspect. Will generate a method corresponding to the body of the advice with an
 * attribute including additional information.
 * 
 * @author Jim Hugunin
 */
public class AdviceDeclaration extends AjMethodDeclaration {
	public PointcutDesignator pointcutDesignator; // set during parsing
	int baseArgumentCount; // referenced by IfPseudoToken.makeArguments

	public Argument extraArgument; // set during parsing, referenced by Proceed

	public AdviceKind kind; // set during parsing, referenced by Proceed and AsmElementFormatter
	private int extraArgumentFlags = 0;
	
	public int adviceSequenceNumberInType;

	public MethodBinding proceedMethodBinding; // set during this.resolveStaments, referenced by Proceed
	public List proceedCalls = new ArrayList(2); // populated during Proceed.findEnclosingAround

	private boolean proceedInInners;
	private ResolvedMember[] proceedCallSignatures;
	private boolean[] formalsUnchangedToProceed;
	private UnresolvedType[] declaredExceptions;

	public AdviceDeclaration(CompilationResult result, AdviceKind kind) {
		super(result);
		this.returnType = TypeReference.baseTypeReference(T_void, 0,null);
		this.kind = kind;
	}

	// override
	protected int generateInfoAttributes(ClassFile classFile) {
		List l = new ArrayList(1);
		l.add(new EclipseAttributeAdapter(makeAttribute()));
		addDeclarationStartLineAttribute(l, classFile);

		return classFile.generateMethodInfoAttributes(binding, l);
	}

	private AjAttribute makeAttribute() {
		if (kind == AdviceKind.Around) {
			return new AjAttribute.AdviceAttribute(kind, pointcutDesignator.getPointcut(), extraArgumentFlags, sourceStart,
					sourceEnd, null, proceedInInners, proceedCallSignatures, formalsUnchangedToProceed, declaredExceptions);
		} else {
			return new AjAttribute.AdviceAttribute(kind, pointcutDesignator.getPointcut(), extraArgumentFlags, sourceStart,
					sourceEnd, null);
		}
	}

	// override
	public void resolveStatements() {
		if (binding == null || ignoreFurtherInvestigation)
			return;

		ClassScope upperScope = (ClassScope) scope.parent; // !!! safety

		modifiers = checkAndSetModifiers(modifiers, upperScope);
		int bindingModifiers = (modifiers | (binding.modifiers & ExtraCompilerModifiers.AccGenericSignature));
		binding.modifiers = bindingModifiers;

		if (kind == AdviceKind.AfterThrowing && extraArgument != null) {
			TypeBinding argTb = extraArgument.binding.type;
			TypeBinding expectedTb = upperScope.getJavaLangThrowable();
			if (!argTb.isCompatibleWith(expectedTb)) {
				scope.problemReporter().typeMismatchError(argTb, expectedTb, extraArgument,null);
				ignoreFurtherInvestigation = true;
				return;
			}
		}

		pointcutDesignator.finishResolveTypes(this, this.binding, baseArgumentCount, upperScope.referenceContext.binding);

		if (binding == null || ignoreFurtherInvestigation)
			return;

		if (kind == AdviceKind.Around) {
			ReferenceBinding[] exceptions = new ReferenceBinding[] { upperScope.getJavaLangThrowable() };
			proceedMethodBinding = new MethodBinding(Modifier.STATIC | Flags.AccSynthetic, "proceed".toCharArray(),
					binding.returnType, resize(baseArgumentCount + 1, binding.parameters), exceptions, binding.declaringClass);
			proceedMethodBinding.selector = CharOperation.concat(selector, proceedMethodBinding.selector);
		}

		super.resolveStatements(); // upperScope);
		if (binding != null)
			determineExtraArgumentFlags();

		if (kind == AdviceKind.Around) {
			int n = proceedCalls.size();
			// EclipseFactory world = EclipseFactory.fromScopeLookupEnvironment(upperScope);

			// System.err.println("access to: " + Arrays.asList(handler.getMembers()));

			// XXX set these correctly
			formalsUnchangedToProceed = new boolean[baseArgumentCount];
			proceedCallSignatures = new ResolvedMember[0];
			proceedInInners = false;
			declaredExceptions = new UnresolvedType[0];

			for (int i = 0; i < n; i++) {
				Proceed call = (Proceed) proceedCalls.get(i);
				if (call.inInner) {
					// System.err.println("proceed in inner: " + call);
					proceedInInners = true;
					// XXX wrong
					// proceedCallSignatures[i] = world.makeResolvedMember(call.binding);
				}
			}

			// ??? should reorganize into AspectDeclaration
			// if we have proceed in inners we won't ever be inlined so the code below is unneeded
			if (!proceedInInners) {
				PrivilegedHandler handler = (PrivilegedHandler) upperScope.referenceContext.binding.privilegedHandler;
				if (handler == null) {
					handler = new PrivilegedHandler((AspectDeclaration) upperScope.referenceContext);
					// upperScope.referenceContext.binding.privilegedHandler = handler;
				}

				this.traverse(new MakeDeclsPublicVisitor(), (ClassScope) null);

				AccessForInlineVisitor v = new AccessForInlineVisitor((AspectDeclaration) upperScope.referenceContext, handler);
				ContextToken tok = CompilationAndWeavingContext.enteringPhase(CompilationAndWeavingContext.ACCESS_FOR_INLINE,
						selector);
				this.traverse(v, (ClassScope) null);
				CompilationAndWeavingContext.leavingPhase(tok);

				// ??? if we found a construct that we can't inline, set
				// proceedInInners so that we won't try to inline this body
				if (!v.isInlinable)
					proceedInInners = true;
			}
		}
	}

	// called by Proceed.resolveType
	public int getDeclaredParameterCount() {
		// this only works before code generation
		return this.arguments.length - 3 - ((extraArgument == null) ? 0 : 1);
		// Advice.countOnes(extraArgumentFlags);
	}

	private void generateProceedMethod(ClassScope classScope, ClassFile classFile) {
		MethodBinding binding = proceedMethodBinding;

		classFile.generateMethodInfoHeader(binding);
		int methodAttributeOffset = classFile.contentsOffset;
		int attributeNumber = classFile.generateMethodInfoAttributes(binding, AstUtil.getAjSyntheticAttribute());
		int codeAttributeOffset = classFile.contentsOffset;
		classFile.generateCodeAttributeHeader();
		CodeStream codeStream = classFile.codeStream;
		codeStream.reset(this, classFile);

		// push the closure
		int nargs = binding.parameters.length;
		int closureIndex = 0;
		for (int i = 0; i < nargs - 1; i++) {
			closureIndex += AstUtil.slotsNeeded(binding.parameters[i]);
		}

		codeStream.aload(closureIndex);

		// build the Object[]

		codeStream.generateInlinedValue(nargs - 1);
		codeStream.newArray(new ArrayBinding(classScope.getType(TypeConstants.JAVA_LANG_OBJECT,
				TypeConstants.JAVA_LANG_OBJECT.length), 1, classScope.environment()));

		int index = 0;
		for (int i = 0; i < nargs - 1; i++) {
			TypeBinding type = binding.parameters[i];
			codeStream.dup();
			codeStream.generateInlinedValue(i);
			codeStream.load(type, index);
			index += AstUtil.slotsNeeded(type);
			if (type.isBaseType()) {
				codeStream.invoke(Opcodes.OPC_invokestatic, AjTypeConstants.getConversionMethodToObject(classScope, type), null);
			}

			codeStream.aastore();
		}

		// call run
		ReferenceBinding closureType = (ReferenceBinding) binding.parameters[nargs - 1];
		MethodBinding runMethod = closureType.getMethods("run".toCharArray())[0];
		codeStream.invoke(Opcodes.OPC_invokevirtual, runMethod, null);

		TypeBinding returnType = binding.returnType;
		if (returnType.isBaseType()) {
			codeStream.invoke(Opcodes.OPC_invokestatic, AjTypeConstants.getConversionMethodFromObject(classScope, returnType), null);
		} else {
			codeStream.checkcast(returnType);
		}
		AstUtil.generateReturn(returnType, codeStream);
		codeStream.recordPositionsFrom(0, 1);
		classFile.completeCodeAttribute(codeAttributeOffset);
		attributeNumber++;
		classFile.completeMethodInfo(binding,methodAttributeOffset, attributeNumber);
	}

	// override
	public void generateCode(ClassScope classScope, ClassFile classFile) {
		if (ignoreFurtherInvestigation)
			return;

		super.generateCode(classScope, classFile);
		if (proceedMethodBinding != null) {
			generateProceedMethod(classScope, classFile);
		}
	}

	private void determineExtraArgumentFlags() {
		if (extraArgument != null)
			extraArgumentFlags |= Advice.ExtraArgument;

		ThisJoinPointVisitor tjp = new ThisJoinPointVisitor(this);
		extraArgumentFlags |= tjp.removeUnusedExtraArguments();
	}

	private static TypeBinding[] resize(int newSize, TypeBinding[] bindings) {
		int len = bindings.length;
		TypeBinding[] ret = new TypeBinding[newSize];
		System.arraycopy(bindings, 0, ret, 0, Math.min(newSize, len));
		return ret;
	}

	/**
	 * Add either the @Before, @After, @Around, @AfterReturning or @AfterThrowing annotation
	 */
	public void addAtAspectJAnnotations() {
		Annotation adviceAnnotation = null;
		String pointcutExpression = pointcutDesignator.getPointcut().toString();
		String extraArgumentName = "";
		if (extraArgument != null) {
			extraArgumentName = new String(extraArgument.name);
		}
		String argNames = buildArgNameRepresentation();

		if (kind == AdviceKind.Before) {
			adviceAnnotation = AtAspectJAnnotationFactory.createBeforeAnnotation(pointcutExpression, argNames,
					declarationSourceStart);
		} else if (kind == AdviceKind.After) {
			adviceAnnotation = AtAspectJAnnotationFactory.createAfterAnnotation(pointcutExpression, argNames,
					declarationSourceStart);
		} else if (kind == AdviceKind.AfterReturning) {
			adviceAnnotation = AtAspectJAnnotationFactory.createAfterReturningAnnotation(pointcutExpression, argNames,
					extraArgumentName, declarationSourceStart);
		} else if (kind == AdviceKind.AfterThrowing) {
			adviceAnnotation = AtAspectJAnnotationFactory.createAfterThrowingAnnotation(pointcutExpression, argNames,
					extraArgumentName, declarationSourceStart);
		} else if (kind == AdviceKind.Around) {
			adviceAnnotation = AtAspectJAnnotationFactory.createAroundAnnotation(pointcutExpression, argNames,
					declarationSourceStart);
		}
		AtAspectJAnnotationFactory.addAnnotation(this, adviceAnnotation, this.scope);
	}

	private String buildArgNameRepresentation() {
		StringBuffer args = new StringBuffer();
		int numArgsWeCareAbout = getDeclaredParameterCount();
		if (this.arguments != null) {
			for (int i = 0; i < numArgsWeCareAbout; i++) {
				if (i != 0)
					args.append(",");
				args.append(new String(this.arguments[i].name));
			}
		}
		if (extraArgument != null) {
			if (numArgsWeCareAbout > 0) {
				args.append(",");
			}
			args.append(new String(extraArgument.name));
		}
		return args.toString();
	}

	// override, Called by ClassScope.postParse
	public void postParse(TypeDeclaration typeDec) {
		AspectDeclaration aspectDecl = (AspectDeclaration) typeDec;
		adviceSequenceNumberInType = aspectDecl.adviceCounter++;

		StringBuffer stringifiedPointcut = new StringBuffer(30);
		pointcutDesignator.print(0, stringifiedPointcut);
		this.selector = NameMangler.adviceName(EclipseFactory.getName(typeDec.binding).replace('.', '_'), kind,
				adviceSequenceNumberInType, stringifiedPointcut.toString().hashCode()).toCharArray();
		if (arguments != null) {
			baseArgumentCount = arguments.length;
		}

		if (kind == AdviceKind.Around) {
			extraArgument = makeFinalArgument("ajc$aroundClosure", AjTypeConstants.getAroundClosureType());
		}

		int addedArguments = 3;
		if (extraArgument != null) {
			addedArguments += 1;
		}

		arguments = extendArgumentsLength(arguments, addedArguments);

		int index = baseArgumentCount;
		if (extraArgument != null) {
			arguments[index++] = extraArgument;
		}

		arguments[index++] = makeFinalArgument("thisJoinPointStaticPart", AjTypeConstants.getJoinPointStaticPartType());
		arguments[index++] = makeFinalArgument("thisJoinPoint", AjTypeConstants.getJoinPointType());
		arguments[index++] = makeFinalArgument("thisEnclosingJoinPointStaticPart", AjTypeConstants.getJoinPointStaticPartType());

		if (pointcutDesignator.isError()) {
			this.ignoreFurtherInvestigation = true;
		}
		pointcutDesignator.postParse(typeDec, this);
	}

	private int checkAndSetModifiers(int modifiers, ClassScope scope) {
		if (modifiers == 0)
			return Modifier.PUBLIC;
		else if (modifiers == Modifier.STRICT)
			return Modifier.PUBLIC | Modifier.STRICT;
		else {
			tagAsHavingErrors();
			scope.problemReporter().signalError(declarationSourceStart, sourceStart - 1,
					"illegal modifier on advice, only strictfp is allowed");
			return Modifier.PUBLIC;
		}
	}

	// called by IfPseudoToken
	public static Argument[] addTjpArguments(Argument[] arguments, TypeDeclaration containingTypeDec) {
		int index = arguments.length;
		arguments = extendArgumentsLength(arguments, 4);

		arguments[index++] = makeFinalArgument("thisJoinPointStaticPart", AjTypeConstants.getJoinPointStaticPartType());
		arguments[index++] = makeFinalArgument("thisJoinPoint", AjTypeConstants.getJoinPointType());
		arguments[index++] = makeFinalArgument("thisEnclosingJoinPointStaticPart", AjTypeConstants.getJoinPointStaticPartType());
		arguments[index++] = makeFinalArgument("thisAspectInstance", toReference(containingTypeDec.name));

		return arguments;
	}

	private static TypeReference toReference(char[] typename) {
		if (CharOperation.contains('.', typename)) {
			char[][] compoundName = CharOperation.splitOn('.', typename);
			return new QualifiedTypeReference(compoundName, new long[compoundName.length]);
		} else {
			return new SingleTypeReference(typename, 0);
		}
	}

	private static Argument makeFinalArgument(String name, TypeReference typeRef) {
		long pos = 0; // XXX encode start and end location
		return new Argument(name.toCharArray(), pos, typeRef, Modifier.FINAL);
	}

	private static Argument[] extendArgumentsLength(Argument[] args, int addedArguments) {
		if (args == null) {
			return new Argument[addedArguments];
		}
		int len = args.length;
		Argument[] ret = new Argument[len + addedArguments];
		System.arraycopy(args, 0, ret, 0, len);
		return ret;
	}

	// public String toString(int tab) {
	// String s = tabString(tab);
	// if (modifiers != AccDefault) {
	// s += modifiersString(modifiers);
	// }
	//
	// if (kind == AdviceKind.Around) {
	// s += returnTypeToString(0);
	// }
	//
	//		s += new String(selector) + "("; //$NON-NLS-1$
	// if (arguments != null) {
	// for (int i = 0; i < arguments.length; i++) {
	// s += arguments[i].toString(0);
	// if (i != (arguments.length - 1))
	//					s = s + ", "; //$NON-NLS-1$
	// };
	// };
	//		s += ")"; //$NON-NLS-1$
	//
	// if (extraArgument != null) {
	// s += "(" + extraArgument.toString(0) + ")";
	// }
	//
	//
	//
	// if (thrownExceptions != null) {
	//			s += " throws "; //$NON-NLS-1$
	// for (int i = 0; i < thrownExceptions.length; i++) {
	// s += thrownExceptions[i].toString(0);
	// if (i != (thrownExceptions.length - 1))
	//					s = s + ", "; //$NON-NLS-1$
	// };
	// };
	//
	// s += ": ";
	// if (pointcutDesignator != null) {
	// s += pointcutDesignator.toString(0);
	// }
	//
	// s += toStringStatements(tab + 1);
	// return s;
	// }

	public StringBuffer printBody(int indent, StringBuffer output) {
		output.append(": ");
		if (pointcutDesignator != null) {
			output.append(pointcutDesignator.toString());
		}
		return super.printBody(indent, output);
	}

	public StringBuffer printReturnType(int indent, StringBuffer output) {
		if (this.kind == AdviceKind.Around) {
			return super.printReturnType(indent, output);
		}
		return output;
	}
}
