/*******************************************************************************
 * Copyright (c) 2005 Contributors.
 * All rights reserved.
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Public License v 2.0
 * which accompanies this distribution and is available at
 * https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt
 *
 * Contributors:
 *   Alexandre Vasseur         initial implementation
 *******************************************************************************/
package org.aspectj.weaver.bcel;

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

import org.aspectj.apache.bcel.Constants;
import org.aspectj.apache.bcel.classfile.ConstantPool;
import org.aspectj.apache.bcel.generic.FieldInstruction;
import org.aspectj.apache.bcel.generic.Instruction;
import org.aspectj.apache.bcel.generic.InstructionConstants;
import org.aspectj.apache.bcel.generic.InstructionFactory;
import org.aspectj.apache.bcel.generic.InstructionHandle;
import org.aspectj.apache.bcel.generic.InstructionList;
import org.aspectj.apache.bcel.generic.InvokeDynamic;
import org.aspectj.apache.bcel.generic.InvokeInstruction;
import org.aspectj.apache.bcel.generic.Type;
import org.aspectj.weaver.AjAttribute;
import org.aspectj.weaver.AjcMemberMaker;
import org.aspectj.weaver.Member;
import org.aspectj.weaver.NameMangler;
import org.aspectj.weaver.ResolvedMember;
import org.aspectj.weaver.ResolvedType;
import org.aspectj.weaver.Shadow;
import org.aspectj.weaver.UnresolvedType;

/**
 * Looks for all access to method or field that are not public within the body of the around advices and replace the invocations to
 * a wrapper call so that the around advice can further be inlined.
 * <p>
 * This munger is used for @AJ aspects for which inlining wrapper is not done at compile time.
 * </p>
 * <p>
 * Specific state and logic is kept in the munger ala ITD so that call/get/set pointcuts can still be matched on the wrapped member
 * thanks to the EffectiveSignature attribute.
 * </p>
 *
 * @author Alexandre Vasseur
 * @author Andy Clement
 */
public class BcelAccessForInlineMunger extends BcelTypeMunger {

	private Map<String, ResolvedMember> inlineAccessors;

	private LazyClassGen aspectGen;

	/**
	 * The wrapper methods representing any created inlineAccessors
	 */
	private Set<LazyMethodGen> inlineAccessorMethodGens;

	public BcelAccessForInlineMunger(ResolvedType aspectType) {
		super(null, aspectType);
		if (aspectType.getWorld().isXnoInline()) {
			throw new Error("This should not happen");
		}
	}

	@Override
	public boolean munge(BcelClassWeaver weaver) {
		aspectGen = weaver.getLazyClassGen();
		inlineAccessors = new HashMap<>(0);
		inlineAccessorMethodGens = new HashSet<>();

		// look for all @Around advices
		for (LazyMethodGen methodGen : aspectGen.getMethodGens()) {
			if (methodGen.hasAnnotation(UnresolvedType.forName("org/aspectj/lang/annotation/Around"))) {
				openAroundAdvice(methodGen);
			}
		}

		// add the accessors
		for (LazyMethodGen lazyMethodGen : inlineAccessorMethodGens) {
			aspectGen.addMethodGen(lazyMethodGen);
		}

		// flush some
		inlineAccessorMethodGens = null;
		// we keep m_inlineAccessorsResolvedMembers for shadow matching

		return true;
	}

	/**
	 * Looks in the wrapper we have added so that we can find their effective signature if needed
	 */
	@Override
	public ResolvedMember getMatchingSyntheticMember(Member member) {
    String name = member.getName();
    String signature = name.startsWith("ajc$superDispatch$")
      ? member.getSignature()
      : member.getSignature().replaceFirst("\\([^;]+;", "(");
    ResolvedMember rm = inlineAccessors.get(name + signature);
//		System.err.println("lookup for " + member.getName() + ":" + member.getSignature() + " = "
//				+ (rm == null ? "" : rm.getName()));
		return rm;
	}

	@Override
	public ResolvedMember getSignature() {
		return null;
	}

	/**
	 * Match only the aspect for which we act
	 */
	@Override
	public boolean matches(ResolvedType onType) {
		return aspectType.equals(onType);
	}

	/**
	 * Prepare the around advice, flag it as cannot be inlined if it can't be
	 */
	private void openAroundAdvice(LazyMethodGen aroundAdvice) {
		InstructionHandle curr = aroundAdvice.getBody().getStart();
		InstructionHandle end = aroundAdvice.getBody().getEnd();
		ConstantPool cpg = aroundAdvice.getEnclosingClass().getConstantPool();
		InstructionFactory factory = aroundAdvice.getEnclosingClass().getFactory();

		boolean realizedCannotInline = false;
		while (curr != end) {
			if (realizedCannotInline) {
				// we know we cannot inline this advice so no need for futher handling
				break;
			}
			InstructionHandle next = curr.getNext();
			Instruction inst = curr.getInstruction();

			// open-up method call
			if ((inst instanceof InvokeInstruction)) {
				InvokeInstruction invoke = (InvokeInstruction) inst;
				if (invoke instanceof InvokeDynamic) {
					realizedCannotInline = true;
					break;
				}
				ResolvedType callee = aspectGen.getWorld().resolve(UnresolvedType.forName(invoke.getClassName(cpg)));

				// look in the whole method list and not just declared for super calls and alike
				List<ResolvedMember> methods = callee.getMethodsWithoutIterator(false, true, false);
				for (ResolvedMember resolvedMember : methods) {
					if (invoke.getName(cpg).equals(resolvedMember.getName())
							&& invoke.getSignature(cpg).equals(resolvedMember.getSignature()) && !resolvedMember.isPublic()) {
						if ("<init>".equals(invoke.getName(cpg))
								) {
							// If ctor invocation, we care about whether it is targeting exactly the same type
							// (ignore non public ctors in supertype of the target) (J13 - AbstractStringBuilder has something
							// that trips this up in one testcase)
							if (invoke.getClassName(cpg).equals(resolvedMember.getDeclaringType().getPackageName()+
									"."+resolvedMember.getDeclaringType().getClassName())) {
								// skipping open up for private constructor
								// can occur when aspect new a private inner type
								// too complex to handle new + dup + .. + invokespecial here.
								aroundAdvice.setCanInline(false);
								realizedCannotInline = true;
							}
						} else {
							// specific handling for super.foo() calls, where foo is non public
							ResolvedType memberType = aspectGen.getWorld().resolve(resolvedMember.getDeclaringType());
							if (!aspectType.equals(memberType) && memberType.isAssignableFrom(aspectType)) {
								// old test was...
								// if (aspectType.getSuperclass() != null
								// && aspectType.getSuperclass().getName().equals(resolvedMember.getDeclaringType().getName())) {
								ResolvedMember accessor = createOrGetInlineAccessorForSuperDispatch(resolvedMember);
								InvokeInstruction newInst = factory.createInvoke(aspectType.getName(), accessor.getName(),
										BcelWorld.makeBcelType(accessor.getReturnType()),
										BcelWorld.makeBcelTypes(accessor.getParameterTypes()), Constants.INVOKEVIRTUAL);
								curr.setInstruction(newInst);
							} else {
								ResolvedMember accessor = createOrGetInlineAccessorForMethod(resolvedMember);
								InvokeInstruction newInst = factory.createInvoke(aspectType.getName(), accessor.getName(),
										BcelWorld.makeBcelType(accessor.getReturnType()),
										BcelWorld.makeBcelTypes(accessor.getParameterTypes()), Constants.INVOKESTATIC);
								curr.setInstruction(newInst);
							}
						}

						break;// ok we found a matching callee member and swapped the instruction with the accessor
					}
				}
			} else if (inst instanceof FieldInstruction) {
				FieldInstruction invoke = (FieldInstruction) inst;
				ResolvedType callee = aspectGen.getWorld().resolve(UnresolvedType.forName(invoke.getClassName(cpg)));
				for (int i = 0; i < callee.getDeclaredJavaFields().length; i++) {
					ResolvedMember resolvedMember = callee.getDeclaredJavaFields()[i];
					if (invoke.getName(cpg).equals(resolvedMember.getName())
							&& invoke.getSignature(cpg).equals(resolvedMember.getSignature()) && !resolvedMember.isPublic()) {
						final ResolvedMember accessor;
						if ((inst.opcode == Constants.GETFIELD) || (inst.opcode == Constants.GETSTATIC)) {
							accessor = createOrGetInlineAccessorForFieldGet(resolvedMember);
						} else {
							accessor = createOrGetInlineAccessorForFieldSet(resolvedMember);
						}
						InvokeInstruction newInst = factory.createInvoke(aspectType.getName(), accessor.getName(),
								BcelWorld.makeBcelType(accessor.getReturnType()),
								BcelWorld.makeBcelTypes(accessor.getParameterTypes()), Constants.INVOKESTATIC);
						curr.setInstruction(newInst);

						break;// ok we found a matching callee member and swapped the instruction with the accessor
					}
				}
			}

			curr = next;
		}

		// no reason for not inlining this advice
		// since it is used for @AJ advice that cannot be inlined by defauilt
		// make sure we set inline to true since we have done this analysis
		if (!realizedCannotInline) {
			aroundAdvice.setCanInline(true);
		}
	}

	/**
	 * Find (or add if not yet created) an inline wrapper for a non public method call
	 */
	private ResolvedMember createOrGetInlineAccessorForMethod(ResolvedMember resolvedMember) {
		String accessorName = NameMangler.inlineAccessMethodForMethod(resolvedMember.getName(), resolvedMember.getDeclaringType(),
				aspectType);
		String key = accessorName + resolvedMember.getSignature();
		ResolvedMember inlineAccessor = inlineAccessors.get(key);
//		System.err.println(key + " accessor=" + inlineAccessor);
		if (inlineAccessor == null) {
			// add static method to aspect
			inlineAccessor = AjcMemberMaker.inlineAccessMethodForMethod(aspectType, resolvedMember);

			// add new accessor method to aspect bytecode
			InstructionFactory factory = aspectGen.getFactory();
			LazyMethodGen method = makeMethodGen(aspectGen, inlineAccessor);
			method.makeSynthetic();
			List<AjAttribute> methodAttributes = new ArrayList<>();
			methodAttributes.add(new AjAttribute.AjSynthetic());
			methodAttributes.add(new AjAttribute.EffectiveSignatureAttribute(resolvedMember, Shadow.MethodCall, false));
			method.addAttribute(Utility.bcelAttribute(methodAttributes.get(0), aspectGen.getConstantPool()));
			// flag the effective signature, so that we can deobfuscate the signature to apply method call pointcut
			method.addAttribute(Utility.bcelAttribute(methodAttributes.get(1), aspectGen.getConstantPool()));

			inlineAccessorMethodGens.add(method);

			InstructionList il = method.getBody();
			int register = 0;
			for (int i = 0, max = inlineAccessor.getParameterTypes().length; i < max; i++) {
				UnresolvedType ptype = inlineAccessor.getParameterTypes()[i];
				Type type = BcelWorld.makeBcelType(ptype);
				il.append(InstructionFactory.createLoad(type, register));
				register += type.getSize();
			}
			il.append(Utility.createInvoke(factory, Modifier.isStatic(resolvedMember.getModifiers()) ? Constants.INVOKESTATIC
					: Constants.INVOKEVIRTUAL, resolvedMember));
			il.append(InstructionFactory.createReturn(BcelWorld.makeBcelType(inlineAccessor.getReturnType())));

			inlineAccessors.put(key, new BcelMethod(aspectGen.getBcelObjectType(), method.getMethod(), methodAttributes));
		}
		return inlineAccessor;
	}

	/**
	 * Add an inline wrapper for a non public super.method call
	 */
	private ResolvedMember createOrGetInlineAccessorForSuperDispatch(ResolvedMember resolvedMember) {
		String accessor = NameMangler.superDispatchMethod(aspectType, resolvedMember.getName());

		String key = accessor + resolvedMember.getSignature();
		ResolvedMember inlineAccessor = inlineAccessors.get(key);

		if (inlineAccessor == null) {
			// add super accessor method to class:
			inlineAccessor = AjcMemberMaker.superAccessMethod(aspectType, resolvedMember);

			// add new accessor method to aspect bytecode
			InstructionFactory factory = aspectGen.getFactory();
			LazyMethodGen method = makeMethodGen(aspectGen, inlineAccessor);
			// flag it synthetic, AjSynthetic
			method.makeSynthetic();
			List<AjAttribute> methodAttributes = new ArrayList<>();
			methodAttributes.add(new AjAttribute.AjSynthetic());
			methodAttributes.add(new AjAttribute.EffectiveSignatureAttribute(resolvedMember, Shadow.MethodCall, false));
			method.addAttribute(Utility.bcelAttribute(methodAttributes.get(0), aspectGen.getConstantPool()));
			// flag the effective signature, so that we can deobfuscate the signature to apply method call pointcut
			method.addAttribute(Utility.bcelAttribute(methodAttributes.get(1), aspectGen.getConstantPool()));

			inlineAccessorMethodGens.add(method);

			InstructionList il = method.getBody();
			il.append(InstructionConstants.ALOAD_0);
			int register = 1;
			for (int i = 0; i < inlineAccessor.getParameterTypes().length; i++) {
				UnresolvedType typeX = inlineAccessor.getParameterTypes()[i];
				Type type = BcelWorld.makeBcelType(typeX);
				il.append(InstructionFactory.createLoad(type, register));
				register += type.getSize();
			}
			il.append(Utility.createInvoke(factory, Constants.INVOKESPECIAL, resolvedMember));
			il.append(InstructionFactory.createReturn(BcelWorld.makeBcelType(inlineAccessor.getReturnType())));

			inlineAccessors.put(key, new BcelMethod(aspectGen.getBcelObjectType(), method.getMethod(), methodAttributes));
		}
		return inlineAccessor;
	}

	/**
	 * Add an inline wrapper for a non public field get
	 */
	private ResolvedMember createOrGetInlineAccessorForFieldGet(ResolvedMember resolvedMember) {
		String accessor = NameMangler.inlineAccessMethodForFieldGet(resolvedMember.getName(), resolvedMember.getDeclaringType(),
				aspectType);
		String key = accessor + "()" + resolvedMember.getSignature();
		ResolvedMember inlineAccessor = inlineAccessors.get(key);

		if (inlineAccessor == null) {
			// add static method to aspect
			inlineAccessor = AjcMemberMaker.inlineAccessMethodForFieldGet(aspectType, resolvedMember);

			// add new accessor method to aspect bytecode
			InstructionFactory factory = aspectGen.getFactory();
			LazyMethodGen method = makeMethodGen(aspectGen, inlineAccessor);
			// flag it synthetic, AjSynthetic
			method.makeSynthetic();
			List<AjAttribute> methodAttributes = new ArrayList<>();
			methodAttributes.add(new AjAttribute.AjSynthetic());
			methodAttributes.add(new AjAttribute.EffectiveSignatureAttribute(resolvedMember, Shadow.FieldGet, false));
			// flag the effective signature, so that we can deobfuscate the signature to apply method call pointcut
			method.addAttribute(Utility.bcelAttribute(methodAttributes.get(0), aspectGen.getConstantPool()));
			method.addAttribute(Utility.bcelAttribute(methodAttributes.get(1), aspectGen.getConstantPool()));

			inlineAccessorMethodGens.add(method);

			InstructionList il = method.getBody();
			if (Modifier.isStatic(resolvedMember.getModifiers())) {
				// field accessed is static so no "this" as accessor sole parameter
			} else {
				il.append(InstructionConstants.ALOAD_0);
			}
			il.append(Utility.createGet(factory, resolvedMember));
			il.append(InstructionFactory.createReturn(BcelWorld.makeBcelType(inlineAccessor.getReturnType())));

			inlineAccessors.put(key, new BcelMethod(aspectGen.getBcelObjectType(), method.getMethod(), methodAttributes));
		}
		return inlineAccessor;
	}

	/**
	 * Add an inline wrapper for a non public field set
	 */
	private ResolvedMember createOrGetInlineAccessorForFieldSet(ResolvedMember resolvedMember) {
		String accessor = NameMangler.inlineAccessMethodForFieldSet(resolvedMember.getName(), resolvedMember.getDeclaringType(),
				aspectType);
		String key = accessor + "(" + resolvedMember.getSignature() + ")V";
		ResolvedMember inlineAccessor = inlineAccessors.get(key);

		if (inlineAccessor == null) {
			// add static method to aspect
			inlineAccessor = AjcMemberMaker.inlineAccessMethodForFieldSet(aspectType, resolvedMember);

			// add new accessor method to aspect bytecode
			InstructionFactory factory = aspectGen.getFactory();
			LazyMethodGen method = makeMethodGen(aspectGen, inlineAccessor);
			// flag it synthetic, AjSynthetic
			method.makeSynthetic();
			List<AjAttribute> methodAttributes = new ArrayList<>();
			methodAttributes.add(new AjAttribute.AjSynthetic());
			methodAttributes.add(new AjAttribute.EffectiveSignatureAttribute(resolvedMember, Shadow.FieldSet, false));
			method.addAttribute(Utility.bcelAttribute(methodAttributes.get(0), aspectGen.getConstantPool()));
			// flag the effective signature, so that we can deobfuscate the signature to apply method call pointcut
			method.addAttribute(Utility.bcelAttribute(methodAttributes.get(1), aspectGen.getConstantPool()));

			inlineAccessorMethodGens.add(method);

			InstructionList il = method.getBody();
			if (Modifier.isStatic(resolvedMember.getModifiers())) {
				// field accessed is static so sole parameter is field value to be set
				il.append(InstructionFactory.createLoad(BcelWorld.makeBcelType(resolvedMember.getReturnType()), 0));
			} else {
				il.append(InstructionConstants.ALOAD_0);
				il.append(InstructionFactory.createLoad(BcelWorld.makeBcelType(resolvedMember.getReturnType()), 1));
			}
			il.append(Utility.createSet(factory, resolvedMember));
			il.append(InstructionConstants.RETURN);
			inlineAccessors.put(key, new BcelMethod(aspectGen.getBcelObjectType(), method.getMethod(), methodAttributes));
		}
		return inlineAccessor;
	}
}
