/*******************************************************************************
 * 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.loadtime;

import static org.aspectj.apache.bcel.generic.Type.NO_ARGS;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.aspectj.apache.bcel.Constants;
import org.aspectj.apache.bcel.classfile.ConstantPool;
import org.aspectj.apache.bcel.classfile.JavaClass;
import org.aspectj.apache.bcel.classfile.annotation.AnnotationGen;
import org.aspectj.apache.bcel.classfile.annotation.ClassElementValue;
import org.aspectj.apache.bcel.classfile.annotation.ElementValue;
import org.aspectj.apache.bcel.classfile.annotation.NameValuePair;
import org.aspectj.apache.bcel.classfile.annotation.SimpleElementValue;
import org.aspectj.apache.bcel.generic.FieldGen;
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.LocalVariableTag;
import org.aspectj.apache.bcel.generic.ObjectType;
import org.aspectj.apache.bcel.generic.Type;
import org.aspectj.bridge.IMessage;
import org.aspectj.bridge.Message;
import org.aspectj.weaver.AjAttribute;
import org.aspectj.weaver.AnnotationAJ;
import org.aspectj.weaver.GeneratedReferenceTypeDelegate;
import org.aspectj.weaver.ReferenceType;
import org.aspectj.weaver.ResolvedMember;
import org.aspectj.weaver.ResolvedType;
import org.aspectj.weaver.UnresolvedType;
import org.aspectj.weaver.World;
import org.aspectj.weaver.bcel.BcelAnnotation;
import org.aspectj.weaver.bcel.BcelPerClauseAspectAdder;
import org.aspectj.weaver.bcel.BcelWorld;
import org.aspectj.weaver.bcel.LazyClassGen;
import org.aspectj.weaver.bcel.LazyMethodGen;
import org.aspectj.weaver.loadtime.definition.Definition;
import org.aspectj.weaver.loadtime.definition.Definition.AdviceKind;
import org.aspectj.weaver.loadtime.definition.Definition.DeclareAnnotationKind;
import org.aspectj.weaver.loadtime.definition.Definition.PointcutAndAdvice;
import org.aspectj.weaver.patterns.BasicTokenSource;
import org.aspectj.weaver.patterns.DeclareAnnotation;
import org.aspectj.weaver.patterns.ISignaturePattern;
import org.aspectj.weaver.patterns.ITokenSource;
import org.aspectj.weaver.patterns.PatternParser;
import org.aspectj.weaver.patterns.PerClause;
import org.aspectj.weaver.patterns.TypePattern;

/**
 * Generates bytecode for concrete-aspect.
 * <p>
 * The concrete aspect is generated annotation style aspect (so traditional Java constructs annotated with our AspectJ annotations).
 * As it is built during aop.xml definitions registration we perform the type munging for perclause, ie. aspectOf() artifact
 * directly, instead of waiting for it to go thru the weaver (that we are in the middle of configuring).
 *
 * @author Alexandre Vasseur
 * @author Andy Clement
 */
public class ConcreteAspectCodeGen {

	private final static String[] EMPTY_STRINGS = new String[0];

	/**
	 * Concrete aspect definition we build for
	 */
	private final Definition.ConcreteAspect concreteAspect;

	/**
	 * World for which we build for
	 */
	private final World world;

	/**
	 * Set to true when all is checks are verified
	 */
	private boolean isValid = false;

	/**
	 * The parent aspect, not concretized
	 */
	private ResolvedType parent;

	/**
	 * Aspect perClause, used for direct munging of aspectOf artifacts
	 */
	private PerClause perclause;

	/**
	 * Bytecode for the generated class
	 */
	private byte[] bytes;

	/**
	 * Create a new generator for a concrete aspect
	 *
	 * @param concreteAspect the aspect definition
	 * @param world the related world (for type resolution, etc)
	 */
	ConcreteAspectCodeGen(Definition.ConcreteAspect concreteAspect, World world) {
		this.concreteAspect = concreteAspect;
		this.world = world;
	}

	/**
	 * Checks that concrete aspect is valid.
	 *
	 * @return true if ok, false otherwise
	 */
	public boolean validate() {
		if (!(world instanceof BcelWorld)) {
			reportError("Internal error: world must be of type BcelWorld");
			return false;
		}

		// name must be undefined so far
		// TODO only convert the name to signature once, probably earlier than this
		ResolvedType current = world.lookupBySignature(UnresolvedType.forName(concreteAspect.name).getSignature());

		if (current != null && !current.isMissing()) {
			reportError("Attempt to concretize but chosen aspect name already defined: " + stringify());
			return false;
		}

		if (concreteAspect.pointcutsAndAdvice.size() != 0) {
			isValid = true;
			return true;
		}

		if (concreteAspect.declareAnnotations.size()!=0) {
			isValid = true;
			return true;
		}

		// it can happen that extends is null, for precedence only declaration
		if (concreteAspect.extend == null && concreteAspect.precedence != null) {
			if (concreteAspect.pointcuts.isEmpty()) {
				isValid = true;
				// m_perClause = new PerSingleton();
				parent = null;
				return true;// no need to checks more in that special case
			} else {
				reportError("Attempt to use nested pointcuts without extends clause: " + stringify());
				return false;
			}
		}

		String parentAspectName = concreteAspect.extend;

		if (parentAspectName.contains("<")) {
			// yikes, generic parent
			parent = world.resolve(UnresolvedType.forName(parentAspectName), true);
			if (parent.isMissing()) {
				reportError("Unable to resolve type reference: " + stringify());
				return false;
			}
			if (parent.isParameterizedType()) {
				UnresolvedType[] typeParameters = parent.getTypeParameters();
				for (UnresolvedType typeParameter : typeParameters) {
					if (typeParameter instanceof ResolvedType && ((ResolvedType) typeParameter).isMissing()) {
						reportError("Unablet to resolve type parameter '" + typeParameter.getName() + "' from " + stringify());
						return false;
					}
				}
			}
		} else {
			parent = world.resolve(concreteAspect.extend, true);
		}
		// handle inner classes
		if (parent.isMissing()) {
			// fallback on inner class lookup mechanism
			String fixedName = concreteAspect.extend;
			int hasDot = fixedName.lastIndexOf('.');
			while (hasDot > 0) {
				char[] fixedNameChars = fixedName.toCharArray();
				fixedNameChars[hasDot] = '$';
				fixedName = new String(fixedNameChars);
				hasDot = fixedName.lastIndexOf('.');
				parent = world.resolve(UnresolvedType.forName(fixedName), true);
				if (!parent.isMissing()) {
					break;
				}
			}
		}

		if (parent.isMissing()) {
			reportError("Cannot find parent aspect for: " + stringify());
			return false;
		}

		// extends must be abstract (allow for special case of Object where just using aspect for deows)
		if (!(parent.isAbstract() || parent.equals(ResolvedType.OBJECT))) {
			reportError("Attempt to concretize a non-abstract aspect: " + stringify());
			return false;
		}

		// m_parent must be aspect (allow for special case of Object where just using aspect for deows)
		if (!(parent.isAspect() || parent.equals(ResolvedType.OBJECT))) {
			reportError("Attempt to concretize a non aspect: " + stringify());
			return false;
		}

		// must have all abstractions defined
		List<String> elligibleAbstractions = new ArrayList<>();

		Collection<ResolvedMember> abstractMethods = getOutstandingAbstractMethods(parent);
		for (ResolvedMember method : abstractMethods) {
			if ("()V".equals(method.getSignature())) {
				String n = method.getName();
				// Allow for the abstract pointcut being from a code style
				// aspect compiled with -1.5 (see test for 128744)
				if (n.startsWith("ajc$pointcut")) {
					n = n.substring(14);
					n = n.substring(0, n.indexOf("$"));
					elligibleAbstractions.add(n);
				} else if (hasPointcutAnnotation(method)) {
					elligibleAbstractions.add(method.getName());
				} else {
					// error, an outstanding abstract method that can't be
					// concretized in XML
					reportError("Abstract method '" + method.toString() + "' cannot be concretized in XML: " + stringify());
					return false;
				}
			} else {
				if (method.getName().startsWith("ajc$pointcut") || hasPointcutAnnotation(method)) {
					// it may be a pointcut but it doesn't meet the requirements for XML concretization
					reportError("Abstract method '"
							+ method.toString()
							+ "' cannot be concretized as a pointcut (illegal signature, must have no arguments, must return void): "
							+ stringify());
					return false;
				} else {
					// error, an outstanding abstract method that can't be
					// concretized in XML
					reportError("Abstract method '" + method.toString() + "' cannot be concretized in XML: " + stringify());
					return false;
				}
			}
		}
		List<String> pointcutNames = new ArrayList<>();
		for (Definition.Pointcut abstractPc : concreteAspect.pointcuts) {
			pointcutNames.add(abstractPc.name);
		}
		for (String elligiblePc : elligibleAbstractions) {
			if (!pointcutNames.contains(elligiblePc)) {
				reportError("Abstract pointcut '" + elligiblePc + "' not configured: " + stringify());
				return false;
			}
		}

		if (concreteAspect.perclause != null) {
			String perclauseString = concreteAspect.perclause;
			if (perclauseString.startsWith("persingleton")) {
			} else if (perclauseString.startsWith("percflow")) {
			} else if (perclauseString.startsWith("pertypewithin")) {
			} else if (perclauseString.startsWith("perthis")) {
			} else if (perclauseString.startsWith("pertarget")) {
			} else if (perclauseString.startsWith("percflowbelow")) {
			} else {
				reportError("Unrecognized per clause specified " + stringify());
				return false;
			}
		}
		isValid = true;
		return isValid;
	}

	private Collection<ResolvedMember> getOutstandingAbstractMethods(ResolvedType type) {
		Map<String, ResolvedMember> collector = new HashMap<>();
		// let's get to the top of the hierarchy and then walk down ...
		// recording abstract methods then removing
		// them if they get defined further down the hierarchy
		getOutstandingAbstractMethodsHelper(type, collector);
		return collector.values();
	}

	// We are trying to determine abstract methods left over at the bottom of a
	// hierarchy that have not been concretized.
	private void getOutstandingAbstractMethodsHelper(ResolvedType type, Map<String, ResolvedMember> collector) {
		if (type == null) {
			return;
		}
		// Get to the top
		if (!type.equals(ResolvedType.OBJECT)) {
			if (type.getSuperclass() != null) {
				getOutstandingAbstractMethodsHelper(type.getSuperclass(), collector);
			}
		}
		ResolvedMember[] rms = type.getDeclaredMethods();
		if (rms != null) {
			for (ResolvedMember member : rms) {
				String key = member.getName() + member.getSignature();
				if (member.isAbstract()) {
					collector.put(key, member);
				} else {
					collector.remove(key);
				}
			}
		}
	}

	/**
	 * Rebuild the XML snip that defines this concrete aspect, for log error purpose
	 *
	 * @return string repr.
	 */
	private String stringify() {
		StringBuilder sb = new StringBuilder("<concrete-aspect name='");
		sb.append(concreteAspect.name);
		sb.append("' extends='");
		sb.append(concreteAspect.extend);
		sb.append("' perclause='");
		sb.append(concreteAspect.perclause);
		sb.append("'/> in aop.xml");
		// TODO needs the extra state from the definition (concretized pointcuts and advice definitions)
		return sb.toString();
	}

	private boolean hasPointcutAnnotation(ResolvedMember member) {
		AnnotationAJ[] as = member.getAnnotations();
		if (as == null || as.length == 0) {
			return false;
		}
		for (AnnotationAJ a : as) {
			if (a.getTypeSignature().equals("Lorg/aspectj/lang/annotation/Pointcut;")) {
				return true;
			}
		}
		return false;
	}

	public String getClassName() {
		return concreteAspect.name;
	}

	/**
	 * @return the bytecode for the concrete aspect
	 */
	public byte[] getBytes() {
		if (!isValid) {
			throw new RuntimeException("Must validate first");
		}
		if (bytes != null) {
			return bytes;
		}
		PerClause.Kind perclauseKind = PerClause.SINGLETON;
		PerClause parentPerClause = (parent != null ? parent.getPerClause() : null);
		if (parentPerClause != null) {
			perclauseKind = parentPerClause.getKind();
		}
		String perclauseString = null;

		if (concreteAspect.perclause != null) {
			perclauseString = concreteAspect.perclause;
			if (perclauseString.startsWith("persingleton")) {
				perclauseKind = PerClause.SINGLETON;
			} else if (perclauseString.startsWith("percflow")) {
				perclauseKind = PerClause.PERCFLOW;
			} else if (perclauseString.startsWith("pertypewithin")) {
				perclauseKind = PerClause.PERTYPEWITHIN;
			} else if (perclauseString.startsWith("perthis")) {
				perclauseKind = PerClause.PEROBJECT;
			} else if (perclauseString.startsWith("pertarget")) {
				perclauseKind = PerClause.PEROBJECT;
			} else if (perclauseString.startsWith("percflowbelow")) {
				perclauseKind = PerClause.PERCFLOW;
			}
		}

		// @Aspect //inherit clause from m_parent
		// @DeclarePrecedence("....") // if any
		// public class xxxName [extends xxxExtends] {
		// [@Pointcut(xxxExpression-n)
		// public void xxxName-n() {}]
		// }
		String parentName = "java/lang/Object";
		if (parent != null) {
			if (parent.isParameterizedType()) {
				parentName = parent.getGenericType().getName().replace('.', '/');
			} else {
				parentName = parent.getName().replace('.', '/');
			}
		}
		// @Aspect public class ...
		// TODO AV - we could point to the aop.xml that defines it and use JSR-45
		LazyClassGen cg = new LazyClassGen(concreteAspect.name.replace('.', '/'), parentName, null, Modifier.PUBLIC
				+ Constants.ACC_SUPER, EMPTY_STRINGS, world);
		if (parent != null && parent.isParameterizedType()) {
			cg.setSuperClass(parent);
		}
		if (perclauseString == null) {
			AnnotationGen ag = new AnnotationGen(new ObjectType("org/aspectj/lang/annotation/Aspect"),
					Collections.<NameValuePair> emptyList(), true, cg.getConstantPool());
			cg.addAnnotation(ag);
		} else {
			// List elems = new ArrayList();
			List<NameValuePair> elems = new ArrayList<>();
			elems.add(new NameValuePair("value",
					new SimpleElementValue(ElementValue.STRING, cg.getConstantPool(), perclauseString), cg.getConstantPool()));
			AnnotationGen ag = new AnnotationGen(new ObjectType("org/aspectj/lang/annotation/Aspect"), elems, true,
					cg.getConstantPool());
			cg.addAnnotation(ag);
		}
		if (concreteAspect.precedence != null) {
			SimpleElementValue svg = new SimpleElementValue(ElementValue.STRING, cg.getConstantPool(), concreteAspect.precedence);
			List<NameValuePair> elems = new ArrayList<>();
			elems.add(new NameValuePair("value", svg, cg.getConstantPool()));
			AnnotationGen agprec = new AnnotationGen(new ObjectType("org/aspectj/lang/annotation/DeclarePrecedence"), elems, true,
					cg.getConstantPool());
			cg.addAnnotation(agprec);
		}

		// default constructor
		LazyMethodGen init = new LazyMethodGen(Modifier.PUBLIC, Type.VOID, "<init>", NO_ARGS, EMPTY_STRINGS, cg);
		InstructionList cbody = init.getBody();
		cbody.append(InstructionConstants.ALOAD_0);

		cbody.append(cg.getFactory().createInvoke(parentName, "<init>", Type.VOID, NO_ARGS, Constants.INVOKESPECIAL));
		cbody.append(InstructionConstants.RETURN);
		cg.addMethodGen(init);

		for (Definition.Pointcut abstractPc : concreteAspect.pointcuts) {
			// TODO AV - respect visibility instead of opening up as public?
			LazyMethodGen mg = new LazyMethodGen(Modifier.PUBLIC, Type.VOID, abstractPc.name, NO_ARGS, EMPTY_STRINGS, cg);
			SimpleElementValue svg = new SimpleElementValue(ElementValue.STRING, cg.getConstantPool(), abstractPc.expression);
			List<NameValuePair> elems = new ArrayList<>();
			elems.add(new NameValuePair("value", svg, cg.getConstantPool()));
			AnnotationGen mag = new AnnotationGen(new ObjectType("org/aspectj/lang/annotation/Pointcut"), elems, true,
					cg.getConstantPool());
			AnnotationAJ max = new BcelAnnotation(mag, world);
			mg.addAnnotation(max);

			InstructionList body = mg.getBody();
			body.append(InstructionConstants.RETURN);
			cg.addMethodGen(mg);
		}

		// Construct any defined declare error/warnings
		if (concreteAspect.deows.size() > 0) {
			int counter = 1;
			for (Definition.DeclareErrorOrWarning deow : concreteAspect.deows) {
				// Building this:
				// @DeclareWarning("call(* javax.sql..*(..)) && !within(org.xyz.daos..*)")
				// static final String aMessage = "Only DAOs should be calling JDBC.";

				FieldGen field = new FieldGen(Modifier.FINAL, ObjectType.STRING, "rule" + (counter++), cg.getConstantPool());
				SimpleElementValue svg = new SimpleElementValue(ElementValue.STRING, cg.getConstantPool(), deow.pointcut);
				List<NameValuePair> elems = new ArrayList<>();
				elems.add(new NameValuePair("value", svg, cg.getConstantPool()));
				AnnotationGen mag = new AnnotationGen(new ObjectType("org/aspectj/lang/annotation/Declare"
						+ (deow.isError ? "Error" : "Warning")), elems, true, cg.getConstantPool());
				field.addAnnotation(mag);

				field.setValue(deow.message);
				cg.addField(field, null);
			}
		}

		if (concreteAspect.pointcutsAndAdvice.size() > 0) {
			int adviceCounter = 1;
			for (PointcutAndAdvice paa : concreteAspect.pointcutsAndAdvice) {
				generateAdviceMethod(paa, adviceCounter, cg);
				adviceCounter++;
			}
		}

		if (concreteAspect.declareAnnotations.size()>0) {
			int decCounter = 1;
			for (Definition.DeclareAnnotation da: concreteAspect.declareAnnotations) {
				generateDeclareAnnotation(da,decCounter++,cg);
			}
		}

		// handle the perClause
		ReferenceType rt = new ReferenceType(ResolvedType.forName(concreteAspect.name).getSignature(), world);
		GeneratedReferenceTypeDelegate grtd = new GeneratedReferenceTypeDelegate(rt);
		grtd.setSuperclass(parent);
		rt.setDelegate(grtd);

		BcelPerClauseAspectAdder perClauseMunger = new BcelPerClauseAspectAdder(rt, perclauseKind);
		perClauseMunger.forceMunge(cg, false);

		// TODO AV - unsafe cast
		// register the fresh new class into the world repository as it does not
		// exist on the classpath anywhere
		JavaClass jc = cg.getJavaClass((BcelWorld) world);
		((BcelWorld) world).addSourceObjectType(jc, true);

		bytes = jc.getBytes();
		return bytes;
	}

	/**
	 * The DeclareAnnotation object encapsulates an method/field/type descriptor and an annotation. This uses a DeclareAnnotation object
	 * captured from the XML (something like '<declare-annotation field="* field1(..)" annotation="@Annot(a='a',fred=false,'abc')"/>')
	 * and builds the same construct that would have existed if the code style variant was used.  This involves creating a member upon
	 * which to hang the real annotation and then creating a classfile level attribute indicating a declare annotation is present
	 * (that includes the signature pattern and a pointer to the real member holding the annotation).
	 *
	 */
	private void generateDeclareAnnotation(Definition.DeclareAnnotation da, int decCounter, LazyClassGen cg) {

		// Here is an example member from a code style declare annotation:
		//void ajc$declare_at_method_1();
		//  RuntimeInvisibleAnnotations: length = 0x6
		//   00 01 00 1B 00 00
		//  RuntimeVisibleAnnotations: length = 0x15
		//   00 01 00 1D 00 03 00 1E 73 00 1F 00 20 73 00 21
		//   00 22 73 00 23
		//  org.aspectj.weaver.MethodDeclarationLineNumber: length = 0x8
		//   00 00 00 02 00 00 00 16
		//  org.aspectj.weaver.AjSynthetic: length = 0x
		//
		//  Code:
		//   Stack=0, Locals=1, Args_size=1
		//   0:	return

		// and at the class level a Declare attribute:
		//		  org.aspectj.weaver.Declare: length = 0x51
		//		   05 00 00 00 03 01 00 05 40 41 6E 6E 6F 01 00 17
		//		   61 6A 63 24 64 65 63 6C 61 72 65 5F 61 74 5F 6D
		//		   65 74 68 6F 64 5F 31 01 01 00 00 00 00 05 05 00
		//		   08 73 61 79 48 65 6C 6C 6F 00 01 04 00 00 00 00
		//		   07 00 00 00 27 00 00 00 34 00 00 00 16 00 00 00
		//		   3C

		AnnotationAJ constructedAnnotation = buildDeclareAnnotation_actualAnnotation(cg, da);
		if (constructedAnnotation==null) {
			return; // error occurred (and was reported), do not continue
		}

		String nameComponent = da.declareAnnotationKind.name().toLowerCase();
		String declareName = new StringBuilder("ajc$declare_at_").append(nameComponent).append("_").append(decCounter).toString();
		LazyMethodGen declareMethod = new LazyMethodGen(Modifier.PUBLIC, Type.VOID, declareName, NO_ARGS, EMPTY_STRINGS, cg);
		InstructionList declareMethodBody = declareMethod.getBody();
		declareMethodBody.append(InstructionFactory.RETURN);
		declareMethod.addAnnotation(constructedAnnotation);

		DeclareAnnotation deca = null;
		ITokenSource tokenSource = BasicTokenSource.makeTokenSource(da.pattern,null);
		PatternParser pp = new PatternParser(tokenSource);

		if (da.declareAnnotationKind==DeclareAnnotationKind.Method || da.declareAnnotationKind==DeclareAnnotationKind.Field) {
			ISignaturePattern isp = (da.declareAnnotationKind==DeclareAnnotationKind.Method?pp.parseCompoundMethodOrConstructorSignaturePattern(true):pp.parseCompoundFieldSignaturePattern());
			deca = new DeclareAnnotation(da.declareAnnotationKind==DeclareAnnotationKind.Method?DeclareAnnotation.AT_METHOD:DeclareAnnotation.AT_FIELD, isp);
		} else if (da.declareAnnotationKind==DeclareAnnotationKind.Type) {
			TypePattern tp = pp.parseTypePattern();
			deca = new DeclareAnnotation(DeclareAnnotation.AT_TYPE,tp);
		}

		deca.setAnnotationMethod(declareName);
		deca.setAnnotationString(da.annotation);
		AjAttribute attribute = new AjAttribute.DeclareAttribute(deca);
		cg.addAttribute(attribute);
		cg.addMethodGen(declareMethod);
	}

	/**
	 * Construct the annotation that the declare wants to add to the target.
	 */
	private AnnotationAJ buildDeclareAnnotation_actualAnnotation(LazyClassGen cg, Definition.DeclareAnnotation da) {
		AnnotationGen anno = buildAnnotationFromString(cg.getConstantPool(),cg.getWorld(),da.annotation);
		if (anno==null) {
			return null;
		} else {
			AnnotationAJ bcelAnnotation = new BcelAnnotation(anno, world);
			return bcelAnnotation;
		}
	}

	// TODO support array values
	// TODO support annotation values
	/**
	 * Build an AnnotationGen object based on a string, for example "@Foo(35,message='string')".  Notice single quotes are fine for
	 * strings as they don't need special handling in the XML.
	 */
	private AnnotationGen buildAnnotationFromString(ConstantPool cp, World w, String annotationString) {
		int paren = annotationString.indexOf('(');
		if (paren==-1) {
			// the easy case, no values
			AnnotationGen aaj = buildBaseAnnotationType(cp,world,annotationString);
			return aaj;
		} else {
			// Discover the name and name/value pairs
			String name = annotationString.substring(0,paren);
			// break the rest into pieces based on the commas
			List<String> values = new ArrayList<>();
			int pos = paren+1;
			int depth = 0;
			int len = annotationString.length();
			int start = pos;
			while (pos<len) {
				char ch = annotationString.charAt(pos);
				if (ch==')' && depth==0) {
					break; // reached the end
				}
				if (ch=='(' || ch=='[') {
					depth++;
				} else if (ch==')' || ch==']') {
					depth--;
				}
				if (ch==',' && depth==0) {
					// found a comma at the right level to end a name/value pair
					values.add(annotationString.substring(start,pos).trim());
					start=pos+1;
				}
				pos++;
			}
			if (start != pos) {
				// there is a last bit to add
				values.add(annotationString.substring(start,pos).trim());
			}
			AnnotationGen aaj = buildBaseAnnotationType(cp,world,name);
			if (aaj==null) {
				return null; // a check failed
			}
			String typename = aaj.getTypeName();
			ResolvedType type = UnresolvedType.forName(typename).resolve(world); // shame it isn't retrievable from the anno
			ResolvedMember[] rms = type.getDeclaredMethods();
			// Add the values
			for (String value: values) {
				int equalsIndex = value.indexOf("=");
				String key = "value";
				if (value.charAt(0)!='\"' && equalsIndex!=-1) {
					key = value.substring(0,equalsIndex).trim();
					value = value.substring(equalsIndex+1).trim();
				}
				boolean keyIsOk = false;
				for (ResolvedMember rm : rms) {
					NameValuePair nvp = null;
					if (rm.getName().equals(key)) {
						// found it!
						keyIsOk = true;
						UnresolvedType rt = rm.getReturnType();
						if (rt.isPrimitiveType()) {
							switch (rt.getSignature().charAt(0)) {
								case 'J': // long
									try {
										long longValue = Long.parseLong(value);
										nvp = new NameValuePair(key, new SimpleElementValue(ElementValue.PRIMITIVE_LONG, cp, longValue), cp);
									} catch (NumberFormatException nfe) {
										reportError("unable to interpret annotation value '" + value + "' as a long");
										return null;
									}
									break;
								case 'S': // short
									try {
										short shortValue = Short.parseShort(value);
										nvp = new NameValuePair(key, new SimpleElementValue(ElementValue.PRIMITIVE_SHORT, cp, shortValue), cp);
									} catch (NumberFormatException nfe) {
										reportError("unable to interpret annotation value '" + value + "' as a short");
										return null;
									}
									break;
								case 'F': // float
									try {
										float floatValue = Float.parseFloat(value);
										nvp = new NameValuePair(key, new SimpleElementValue(ElementValue.PRIMITIVE_FLOAT, cp, floatValue), cp);
									} catch (NumberFormatException nfe) {
										reportError("unable to interpret annotation value '" + value + "' as a float");
										return null;
									}
									break;
								case 'D': // double
									try {
										double doubleValue = Double.parseDouble(value);
										nvp = new NameValuePair(key, new SimpleElementValue(ElementValue.PRIMITIVE_DOUBLE, cp, doubleValue), cp);
									} catch (NumberFormatException nfe) {
										reportError("unable to interpret annotation value '" + value + "' as a double");
										return null;
									}
									break;
								case 'I': // integer
									try {
										int intValue = Integer.parseInt(value);
										nvp = new NameValuePair(key, new SimpleElementValue(ElementValue.PRIMITIVE_INT, cp, intValue), cp);
									} catch (NumberFormatException nfe) {
										reportError("unable to interpret annotation value '" + value + "' as an integer");
										return null;
									}
									break;
								case 'B': // byte
									try {
										byte byteValue = Byte.parseByte(value);
										nvp = new NameValuePair(key, new SimpleElementValue(ElementValue.PRIMITIVE_BYTE, cp, byteValue), cp);
									} catch (NumberFormatException nfe) {
										reportError("unable to interpret annotation value '" + value + "' as a byte");
										return null;
									}
									break;
								case 'C': // char
									if (value.length() < 2) {
										reportError("unable to interpret annotation value '" + value + "' as a char");
										return null;
									}
									nvp = new NameValuePair(key, new SimpleElementValue(ElementValue.PRIMITIVE_CHAR, cp, value.charAt(1)), cp);
									break;
								case 'Z': // boolean
									try {
										boolean booleanValue = Boolean.parseBoolean(value);
										nvp = new NameValuePair(key, new SimpleElementValue(ElementValue.PRIMITIVE_BOOLEAN, cp, booleanValue), cp);
									} catch (NumberFormatException nfe) {
										reportError("unable to interpret annotation value '" + value + "' as a boolean");
										return null;
									}
									break;
								default:
									reportError("not yet supporting XML setting of annotation values of type " + rt.getName());
									return null;
							}
						} else if (UnresolvedType.JL_STRING.equals(rt)) {
							if (value.length() < 2) {
								reportError("Invalid string value specified in annotation string: " + annotationString);
								return null;
							}
							value = value.substring(1, value.length() - 1); // trim the quotes off
							nvp = new NameValuePair(key, new SimpleElementValue(ElementValue.STRING, cp, value), cp);
						} else if (UnresolvedType.JL_CLASS.equals(rt)) {
							// format of class string:
							// Foo.class
							// java.lang.Foo.class
							if (value.length() < 6) {
								reportError("Not a well formed class value for an annotation '" + value + "'");
								return null;
							}
							String clazz = value.substring(0, value.length() - 6);
							boolean qualified = clazz.contains(".");
							if (!qualified) {
								// if not qualified, have to assume java.lang
								clazz = "java.lang." + clazz;
							}
							nvp = new NameValuePair(key, new ClassElementValue(new ObjectType(clazz), cp), cp);
						}
					}
					if (nvp != null) {
						aaj.addElementNameValuePair(nvp);
					}
				}
				if (!keyIsOk) {
					reportError("annotation @"+typename+" does not have a value named "+key);
					return null;
				}
			}
			return aaj;
		}
	}

	private AnnotationGen buildBaseAnnotationType(ConstantPool cp,World world, String typename) {
		String annoname = typename;
		if (annoname.startsWith("@")) {
			annoname= annoname.substring(1);
		}
		ResolvedType annotationType = UnresolvedType.forName(annoname).resolve(world);
		if (!annotationType.isAnnotation()) {
			reportError("declare is not specifying an annotation type :"+typename);
			return null;
		}
		if (!annotationType.isAnnotationWithRuntimeRetention()) {
			reportError("declare is using an annotation type that does not have runtime retention: "+typename);
			return null;
		}
		List<NameValuePair> elems = new ArrayList<>();
		return new AnnotationGen(new ObjectType(annoname), elems, true, cp);
	}

	/**
	 * Construct the annotation that indicates this is a declare
	 */

	/**
	 * The PointcutAndAdvice object encapsulates an advice kind, a pointcut and names a Java method in a particular class. Generate
	 * an annotation style advice that has that pointcut whose implementation delegates to the Java method.
	 */
	private void generateAdviceMethod(PointcutAndAdvice paa, int adviceCounter, LazyClassGen cg) {

		// Check: Verify the class to be called does exist:
		ResolvedType delegateClass = world.resolve(UnresolvedType.forName(paa.adviceClass));
		if (delegateClass.isMissing()) {
			reportError("Class to invoke cannot be found: '" + paa.adviceClass + "'");
			return;
		}

		// Generate a name for this advice, includes advice kind plus a counter
		String adviceName = new StringBuilder("generated$").append(paa.adviceKind.toString().toLowerCase()).append("$advice$")
				.append(adviceCounter).toString();

		// Build the annotation that encapsulates the pointcut
		AnnotationAJ aaj = buildAdviceAnnotation(cg, paa);

		// Chop up the supplied advice method string into its pieces.
		// Example: foo(JoinPoint jp, java.lang.String string)
		// JoinPoint and friends are recognized (so dont need fq package)
		String method = paa.adviceMethod;

		int paren = method.indexOf("(");
		String methodName = method.substring(0, paren);
		String signature = method.substring(paren);

		// Check: signature looks ok
		if (signature.charAt(0) != '(' || !signature.endsWith(")")) {
			reportError("Badly formatted parameter signature: '" + method + "'");
			return;
		}

		// Extract parameter types and names
		List<Type> paramTypes = new ArrayList<>();
		List<String> paramNames = new ArrayList<>();
		if (signature.charAt(1) != ')') {
			// there are parameters to convert into a signature
			StringBuilder convertedSignature = new StringBuilder("(");
			boolean paramsBroken = false;
			int pos = 1;
			while (pos < signature.length() && signature.charAt(pos) != ')' && !paramsBroken) {
				int nextChunkEndPos = signature.indexOf(',', pos);
				if (nextChunkEndPos == -1) {
					nextChunkEndPos = signature.indexOf(')', pos);
				}
				// chunk will be a type plus a space plus a name
				String nextChunk = signature.substring(pos, nextChunkEndPos).trim();
				int space = nextChunk.indexOf(" ");
				ResolvedType resolvedParamType = null;
				if (space == -1) {
					// There is no parameter name, hopefully not needed!
					if (nextChunk.equals("JoinPoint")) {
						nextChunk = "org.aspectj.lang.JoinPoint";
					} else if (nextChunk.equals("JoinPoint.StaticPart")) {
						nextChunk = "org.aspectj.lang.JoinPoint$StaticPart";
					} else if (nextChunk.equals("ProceedingJoinPoint")) {
						nextChunk = "org.aspectj.lang.ProceedingJoinPoint";
					}
					UnresolvedType unresolvedParamType = UnresolvedType.forName(nextChunk);
					resolvedParamType = world.resolve(unresolvedParamType);
				} else {
					String typename = nextChunk.substring(0, space);
					if (typename.equals("JoinPoint")) {
						typename = "org.aspectj.lang.JoinPoint";
					} else if (typename.equals("JoinPoint.StaticPart")) {
						typename = "org.aspectj.lang.JoinPoint$StaticPart";
					} else if (typename.equals("ProceedingJoinPoint")) {
						typename = "org.aspectj.lang.ProceedingJoinPoint";
					}
					UnresolvedType unresolvedParamType = UnresolvedType.forName(typename);
					resolvedParamType = world.resolve(unresolvedParamType);
					String paramname = nextChunk.substring(space).trim();
					paramNames.add(paramname);
				}
				if (resolvedParamType.isMissing()) {
					reportError("Cannot find type specified as parameter: '" + nextChunk + "' from signature '" + signature + "'");
					paramsBroken = true;
				}
				paramTypes.add(Type.getType(resolvedParamType.getSignature()));
				convertedSignature.append(resolvedParamType.getSignature());
				pos = nextChunkEndPos + 1;
			}
			convertedSignature.append(")");
			signature = convertedSignature.toString();
			if (paramsBroken) {
				return;
			}
		}

		Type returnType = Type.VOID;

		// If around advice we must find the actual delegate method and use its return type
		if (paa.adviceKind == AdviceKind.Around) {
			ResolvedMember[] methods = delegateClass.getDeclaredMethods();
			ResolvedMember found = null;
			for (ResolvedMember candidate : methods) {
				if (candidate.getName().equals(methodName)) {
					UnresolvedType[] cparms = candidate.getParameterTypes();
					if (cparms.length == paramTypes.size()) {
						boolean paramsMatch = true;
						for (int i = 0; i < cparms.length; i++) {
							if (!cparms[i].getSignature().equals(paramTypes.get(i).getSignature())) {
								paramsMatch = false;
								break;
							}
						}
						if (paramsMatch) {
							found = candidate;
							break;
						}
					}
				}
			}
			if (found != null) {
				returnType = Type.getType(found.getReturnType().getSignature());
			} else {
				reportError("Unable to find method to invoke.  In class: " + delegateClass.getName() + " cant find "
						+ paa.adviceMethod);
				return;
			}
		}

		// Time to construct the method itself:
		LazyMethodGen advice = new LazyMethodGen(Modifier.PUBLIC, returnType, adviceName, paramTypes.toArray(NO_ARGS), EMPTY_STRINGS, cg);

		InstructionList adviceBody = advice.getBody();

		// Generate code to load the parameters
		int pos = 1; // first slot after 'this'
		for (Type paramType : paramTypes) {
			adviceBody.append(InstructionFactory.createLoad(paramType, pos));
			pos += paramType.getSize();
		}

		// Generate the delegate call
		adviceBody.append(cg.getFactory().createInvoke(paa.adviceClass, methodName, signature + returnType.getSignature(),
				Constants.INVOKESTATIC));

		// Generate the right return
		if (returnType == Type.VOID) {
			adviceBody.append(InstructionConstants.RETURN);
		} else {
			if (returnType.getSignature().length() < 2) {
				String sig = returnType.getSignature();
				if (sig.equals("F")) {
					adviceBody.append(InstructionConstants.FRETURN);
				} else if (sig.equals("D")) {
					adviceBody.append(InstructionConstants.DRETURN);
				} else if (sig.equals("J")) {
					adviceBody.append(InstructionConstants.LRETURN);
				} else {
					adviceBody.append(InstructionConstants.IRETURN);
				}
			} else {
				adviceBody.append(InstructionConstants.ARETURN);
			}
		}
		// Add the annotation
		advice.addAnnotation(aaj);
		InstructionHandle start = adviceBody.getStart();

		// Setup the local variable targeters so that the binding will work
		String sig = concreteAspect.name.replace('.', '/');
		start.addTargeter(new LocalVariableTag("L" + sig + ";", "this", 0, start.getPosition()));
		if (paramNames.size() > 0) {
			for (int i = 0; i < paramNames.size(); i++) {
				start.addTargeter(new LocalVariableTag(paramTypes.get(i).getSignature(), paramNames.get(i), i + 1, start
						.getPosition()));
			}
		}

		// Record the new method in the class
		cg.addMethodGen(advice);
	}

	/**
	 * For the given PointcutAndAdvice build the correct advice annotation.
	 */
	private AnnotationAJ buildAdviceAnnotation(LazyClassGen cg, PointcutAndAdvice paa) {
		SimpleElementValue svg = new SimpleElementValue(ElementValue.STRING, cg.getConstantPool(), paa.pointcut);
		List<NameValuePair> elems = new ArrayList<>();
		elems.add(new NameValuePair("value", svg, cg.getConstantPool()));
		AnnotationGen mag = new AnnotationGen(new ObjectType("org/aspectj/lang/annotation/" + paa.adviceKind.toString()), elems,
				true, cg.getConstantPool());
		AnnotationAJ aaj = new BcelAnnotation(mag, world);
		return aaj;
	}

	/**
	 * Error reporting
	 *
	 * @param message
	 */
	private void reportError(String message) {
		world.getMessageHandler().handleMessage(new Message(message, IMessage.ERROR, null, null));
	}
}
