package net.java.amateras.xlsbeans.xml;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.LoaderClassPath;
import javassist.NotFoundException;
import ognl.Ognl;

/**
 * Creates {@link java.lang.annotation.Annotation} instances
 * dynamically using Javassist.
 * 
 * @author Naoki Takezoe
 */
public class DynamicAnnotationBuilder {
	
	private static ClassLoader loader;
	
	public static void setClassLoader(ClassLoader loader){
		DynamicAnnotationBuilder.loader = loader;
		ClassPool pool = ClassPool.getDefault();
		pool.appendClassPath(new LoaderClassPath(loader));
	}
	
	/**
	 * Creates the annotation instance dynamically using Javaassist.
	 */
	public static Annotation buildAnnotation(Class<?> ann, AnnotationInfo info) throws Exception {
		
		ClassPool pool = ClassPool.getDefault();
		
		try {
			pool.get(ann.getName() + "Impl");
			Class<?> implClass = Class.forName(ann.getName() + "Impl");
			Object obj = implClass.newInstance();
			setParameter(obj, ann, info);
			return (Annotation)obj;
		} catch(NotFoundException ex){
			// ignore
		}
		
		CtClass cc = pool.makeClass(ann.getName() + "Impl");
		cc.setInterfaces(new CtClass[]{pool.get(ann.getName())});
		
		for(Method method : ann.getMethods()){
			// Build Arguments
			List<CtClass> ctArgs = new ArrayList<CtClass>();
			for(Class<?> arg : method.getParameterTypes()){
				ctArgs.add(pool.get(arg.getName()));
			}
			
			String body = null;
			
			if(method.getName().equals("annotationType")){
				body = "{ return " + ann.getName() + ".class; }";
			} else {
				CtField field = CtField.make("public " + method.getReturnType().getName()
						+ " " + method.getName() + ";",  cc);
				cc.addField(field);
				body = "{ return this." + field.getName() + "; }";
			}
			
			
			CtMethod ctMethod = CtNewMethod.make(
					pool.get(method.getReturnType().getName()),
					method.getName(),
					ctArgs.toArray(new CtClass[ctArgs.size()]),
					new CtClass[0],
					body,
					cc);
			
			cc.addMethod(ctMethod);
		}
		
		Object obj = null;
		if(loader!=null){
			obj = cc.toClass(loader).newInstance();
		} else {
			obj = cc.toClass().newInstance();
		}
		
		setParameter(obj, ann, info);
		return (Annotation)obj;
	}
	
	/**
	 * Sets annotation attribute values to the annotation instance.
	 */
	private static void setParameter(Object obj, Class<?> ann, AnnotationInfo info) throws Exception {
		// Set default values
		for(Method method : ann.getMethods()){
			Object defaultValue = method.getDefaultValue();
			if(defaultValue!=null){
				Field field = obj.getClass().getField(method.getName());
				field.set(obj, defaultValue);
			}
		}
		
		for(String key : info.getAnnotationAttributeKeys()){
			try {
				obj.getClass().getMethod(key, new Class[0]);
				Field field = obj.getClass().getField(key);
				field.set(obj, Ognl.getValue(info.getAnnotationAttribute(key),new Object()));
			} catch(NoSuchMethodException ex){
				// ignore
			}
		}
//		// TODO How can I get the default value of annotation parameter?
//		if(info.getAnnotationClass().equals(LabelledCell.class.getName())){
//			setDefaultValue(obj, info, "range", 1);
//		}
	}
	
//	private static void setDefaultValue(
//			Object obj, AnnotationInfo info, String name, Object value) throws Exception {
//		
//		if(info.getAnnotationAttribute(name)==null){
//			try {
//				obj.getClass().getMethod(name, new Class[0]);
//				Field field = obj.getClass().getField(name);
//				field.set(obj, value);
//			} catch(NoSuchMethodException ex){
//				// ignore
//			}
//		}
//	}

}
