/*
  * JBoss, Home of Professional Open Source
  * Copyright 2005, JBoss Inc., and individual contributors as indicated
  * by the @authors tag. See the copyright.txt in the distribution for a
  * full listing of individual contributors.
  *
  * This is free software; you can redistribute it and/or modify it
  * under the terms of the GNU Lesser General Public License as
  * published by the Free Software Foundation; either version 2.1 of
  * the License, or (at your option) any later version.
  *
  * This software is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  * Lesser General Public License for more details.
  *
  * You should have received a copy of the GNU Lesser General Public
  * License along with this software; if not, write to the Free
  * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
  * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
  */
package org.jboss.annotation.factory;

import java.io.StringReader;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.HashMap;
import java.util.Properties;

import org.jboss.annotation.factory.ast.ASTAnnotation;
import org.jboss.annotation.factory.ast.ASTChar;
import org.jboss.annotation.factory.ast.ASTIdentifier;
import org.jboss.annotation.factory.ast.ASTMemberValueArrayInitializer;
import org.jboss.annotation.factory.ast.ASTMemberValuePair;
import org.jboss.annotation.factory.ast.ASTMemberValuePairs;
import org.jboss.annotation.factory.ast.ASTSingleMemberValue;
import org.jboss.annotation.factory.ast.ASTStart;
import org.jboss.annotation.factory.ast.ASTString;
import org.jboss.annotation.factory.ast.AnnotationParser;
import org.jboss.annotation.factory.ast.AnnotationParserVisitor;
import org.jboss.annotation.factory.ast.Node;
import org.jboss.annotation.factory.ast.SimpleNode;
import org.jboss.annotation.factory.javassist.DefaultValueAnnotationValidator;
import org.jboss.logging.Logger;
import org.jboss.util.StringPropertyReplacer;

/**
 * Comment
 *
 * @author <a href="mailto:bill@jboss.org">Bill Burke</a>
 * @author <a href="mailto:kabir.khan@jboss.org">Kabir Khan</a>
 * @author <a href="mailto:ales.justin@jboss.org">Ales Justin</a>
 * @version $Revision: 88879 $
 */
public class AnnotationCreator implements AnnotationParserVisitor
{
   private static final Logger log = Logger.getLogger(AnnotationCreator.class);

   private Class<?> annotation;
   private Class<?> type;
   public Object typeValue;
   private ClassLoader loader;
   
   static final AnnotationValidator defaultAnnotationReader;
   static
   {
      boolean haveJavassist = false;
      try
      {
         Class.forName("javassist.CtClass");
         haveJavassist = true;
      }
      catch(ClassNotFoundException ignore)
      {
      }
      
      if (haveJavassist)
      {
         defaultAnnotationReader = new DefaultValueAnnotationValidator();
      }
      else
      {
         defaultAnnotationReader = new SimpleAnnotationValidator();
      }
      
   }

   public AnnotationCreator(Class<?> annotation, Class<?> type, ClassLoader loader)
   {
      this.type = type;
      this.annotation = annotation;
      this.loader = loader;
   }

   public AnnotationCreator(Class<?> annotation, Class<?> type)
   {
      this.type = type;
      this.annotation = annotation;
      this.loader = Thread.currentThread().getContextClassLoader();
   }

   public Object visit(ASTMemberValuePairs node, Object data)
   {
      node.childrenAccept(this, data);
      return null;
   }

   public Object visit(ASTMemberValuePair node, Object data)
   {
      String name = node.getIdentifier().getValue();
      node.getValue().jjtAccept(this, name);
      return data;
   }

   public Object visit(ASTSingleMemberValue node, Object data)
   {
      node.getValue().jjtAccept(this, "value");
      return data;
   }

   public Object visit(ASTIdentifier node, Object data)
   {
      try
      {
         if (type.equals(Class.class))
         {
            String classname = node.getValue();
            if (classname.endsWith(".class"))
            {
               classname = classname.substring(0, classname.indexOf(".class"));
            }
            if (classname.equals("void"))
            {
               typeValue = void.class;
            }
            else if (classname.equals("int"))
            {
               typeValue = int.class;
            }
            else if (classname.equals("byte"))
            {
               typeValue = byte.class;
            }
            else if (classname.equals("long"))
            {
               typeValue = long.class;
            }
            else if (classname.equals("double"))
            {
               typeValue = double.class;
            }
            else if (classname.equals("float"))
            {
               typeValue = float.class;
            }
            else if (classname.equals("char"))
            {
               typeValue = char.class;
            }
            else if (classname.equals("short"))
            {
               typeValue = short.class;
            }
            else if (classname.equals("boolean"))
            {
               typeValue = boolean.class;
            }
            else
            {
               typeValue = Class.forName(classname, false, loader);
            }
         }
         else if (type.isPrimitive())
         {
            if (type.equals(boolean.class))
            {
               typeValue = Boolean.valueOf(node.getValue());
            }
            else if (type.equals(short.class))
            {
               typeValue = Short.valueOf(node.getValue());
            }
            else if (type.equals(float.class))
            {
               typeValue = Float.valueOf(node.getValue());
            }
            else if (type.equals(double.class))
            {
               typeValue = Double.valueOf(node.getValue());
            }
            else if (type.equals(long.class))
            {
               typeValue = Long.valueOf(node.getValue());
            }
            else if (type.equals(byte.class))
            {
               typeValue = new Byte(node.getValue());
            }
            else if (type.equals(int.class))
            {
               typeValue = new Integer(node.getValue());
            }
         }
         else // its an enum
         {
            int index = node.getValue().lastIndexOf('.');
            if (index == -1)
               throw new RuntimeException("Enum must be fully qualified: " + node.getValue());

            String className = node.getValue().substring(0, index);
            String en = node.getValue().substring(index + 1);
            Class<?> enumClass = loader.loadClass(className);

            Class<?> superClass = enumClass.getSuperclass();
            if ("java.lang.Enum".equals(superClass.getName()))
            {
               Method valueOf = superClass.getMethod("valueOf", Class.class, String.class);
               Object[] args = {enumClass, en};
               typeValue = valueOf.invoke(null, args);
            }
            else
            {
               Field field = enumClass.getField(en);
               typeValue = field.get(null);
            }
         }
      }
      catch (ClassNotFoundException e)
      {
         throw new RuntimeException(e);
      }
      catch (IllegalAccessException e)
      {
         throw new RuntimeException(e);
      }
      catch (InvocationTargetException e)
      {
         throw new RuntimeException(e);
      }
      catch (NoSuchFieldException e)
      {
         throw new RuntimeException(e);
      }
      catch (NoSuchMethodException e)
      {
         throw new RuntimeException(e);
      }
      return null;
   }

   public Object visit(ASTString node, Object data)
   {
      if (!type.equals(String.class)) throw new RuntimeException(annotation.getName() + "." + data + " is not an String");
      typeValue = node.getValue();
      return null;
   }

   public Object visit(ASTChar node, Object data)
   {
      if (!type.equals(char.class)) throw new RuntimeException(annotation.getName() + "." + data + " is not an char");
      typeValue = node.getValue();
      return null;
   }


   public Object visit(ASTMemberValueArrayInitializer node, Object data)
   {
      if (!type.isArray()) throw new RuntimeException(annotation.getName() + "." + data + " is not an array");
      Class<?> baseType = type.getComponentType();
      int size = node.jjtGetNumChildren();
      typeValue = Array.newInstance(baseType, size);

      for (int i = 0; i < size; i++)
      {
         AnnotationCreator creator = new AnnotationCreator(annotation, baseType, loader);
         node.jjtGetChild(i).jjtAccept(creator, null);
         Array.set(typeValue, i, creator.typeValue);
      }
      return null;
   }

   public Object visit(ASTAnnotation node, Object data)
   {
      try
      {
         Class<?> subAnnotation = loader.loadClass(node.getIdentifier());
         typeValue = createAnnotation(node, subAnnotation);
      }
      catch (Exception e)
      {
         throw new RuntimeException(e);
      }
      return null;
   }

   // Unneeded

   public Object visit(SimpleNode node, Object data)
   {
      return null;
   }

   public Object visit(ASTStart node, Object data)
   {
      return null;
   }

   private static Class<?> getMemberType(Class<?> annotation, String member)
   {
      Method[] methods = annotation.getMethods();
      for (Method method : methods)
      {
         if (method.getName().equals(member))
         {
            return method.getReturnType();
         }
      }
      throw new RuntimeException("unable to determine member type for annotation: " + annotation.getName() + "." + member);
   }
   
   private static ASTAnnotation getRootExpr(final String annotationExpr) throws Exception
   {
      return getRootExpr(annotationExpr, null, false);
   }

   private static ASTAnnotation getRootExpr(final String annotationExpr, final Properties properties) throws Exception
   {
      return getRootExpr(annotationExpr, properties, null);
   }

   private static ASTAnnotation getRootExpr(final String annotationExpr, final Properties properties, final Boolean replace) throws Exception
   {
      try
      {
         return AccessController.doPrivileged(new PrivilegedExceptionAction<ASTAnnotation>()
         {
            public ASTAnnotation run() throws Exception
            {
               String expr;
               if (properties != null && properties.isEmpty() == false)
                  expr = StringPropertyReplacer.replaceProperties(annotationExpr, properties);
               else if (replace != null && replace)
                  expr = StringPropertyReplacer.replaceProperties(annotationExpr);
               else
                  expr = annotationExpr;
               AnnotationParser parser = new AnnotationParser(new StringReader(expr));
               ASTStart start = parser.Start();
               return (ASTAnnotation)start.jjtGetChild(0);
            }
         });
      }
      catch (PrivilegedActionException e)
      {
         throw new RuntimeException("Error getting root expression " + annotationExpr, e.getException());
      }
   }

   public static Object createAnnotation(ASTAnnotation node, Class<?> annotation, ClassLoader cl) throws Exception
   {
      HashMap<String, Object> map = new HashMap<String, Object>();
      ClassLoader loader;
      if (cl != null)
      {
         loader = cl;
      }
      else
      {
         String info = (annotation != null) ? annotation.getName() : node.getIdentifier();
         if (log.isTraceEnabled())
            log.trace("No ClassLoader provided, using TCCL: " + info);
         loader = Thread.currentThread().getContextClassLoader();
      }
      
      if (annotation == null)
      {
         annotation = loader.loadClass(node.getIdentifier());
      }
      
      if (node.jjtGetNumChildren() > 0)
      {
         Node contained = node.jjtGetChild(0);
         if (contained instanceof ASTSingleMemberValue)
         {
            Class<?> type = getMemberType(annotation, "value");
            AnnotationCreator creator = new AnnotationCreator(annotation, type, loader);
            contained.jjtAccept(creator, "value");
            map.put("value", creator.typeValue);
         }
         else
         {
            ASTMemberValuePairs pairs = (ASTMemberValuePairs) contained;
            for (int i = 0; i < pairs.jjtGetNumChildren(); i++)
            {
               ASTMemberValuePair member = (ASTMemberValuePair) pairs.jjtGetChild(i);
               Class<?> type = getMemberType(annotation, member.getIdentifier().getValue());
               AnnotationCreator creator = new AnnotationCreator(annotation, type, loader);
               member.jjtAccept(creator, null);
               map.put(member.getIdentifier().getValue(), creator.typeValue);
            }
         }
      }
      
      defaultAnnotationReader.validate(map, annotation);
      return AnnotationProxy.createProxy(map, annotation);
   }

   /**
    * Create annotation.
    *
    * @param node the ast annotation node
    * @param annotation the annotation class
    * @return new annotation instance
    * @throws Exception for any error
    */
   public static Object createAnnotation(ASTAnnotation node, Class<?> annotation) throws Exception
   {
      return createAnnotation(node, annotation, null);
   }

   /**
    * Create annotation.
    *
    * @param annotationExpr the annotation expression
    * @param annotation the annotation class
    * @return new annotation instance
    * @throws Exception for any error
    */
   public static Object createAnnotation(final String annotationExpr, final Class<?> annotation) throws Exception
   {
      return createAnnotation(getRootExpr(annotationExpr), annotation, null);
   }

   /**
    * Create annotation.
    *
    * @param annotationExpr the annotation expression
    * @param cl the classloader to use
    * @return new annotation instance
    * @throws Exception for any error
    */
   public static Object createAnnotation(String annotationExpr, ClassLoader cl) throws Exception
   {
      return createAnnotation(getRootExpr(annotationExpr), null, cl);
   }

   /**
    * Create annotation.
    *
    * @param annotationExpr the annotation expression
    * @param annotation the annotation class
    * @param replace should we replace possible properties in expression
    * @return new annotation instance
    * @throws Exception for any error
    */
   public static Object createAnnotation(final String annotationExpr, final Class<?> annotation, boolean replace) throws Exception
   {
      return createAnnotation(getRootExpr(annotationExpr, null, replace), annotation, null);
   }

   /**
    * Create annotation.
    *
    * @param annotationExpr the annotation expression
    * @param cl the classloader to use
    * @param replace should we replace possible properties in expression
    * @return new annotation instance
    * @throws Exception for any error
    */
   public static Object createAnnotation(String annotationExpr, ClassLoader cl, boolean replace) throws Exception
   {
      return createAnnotation(getRootExpr(annotationExpr, null, replace), null, cl);
   }

   /**
    * Create annotation.
    *
    * @param annotationExpr the annotation expression
    * @param annotation the annotation class
    * @param properties the properties to use for replacement
    * @return new annotation instance
    * @throws Exception for any error
    */
   public static Object createAnnotation(final String annotationExpr, final Class<?> annotation, Properties properties) throws Exception
   {
      return createAnnotation(getRootExpr(annotationExpr, properties), annotation, null);
   }

   /**
    * Create annotation.
    *
    * @param annotationExpr the annotation expression
    * @param cl the classloader to use
    * @param properties the properties to use for replacement
    * @return new annotation instance
    * @throws Exception for any error
    */
   public static Object createAnnotation(String annotationExpr, ClassLoader cl, Properties properties) throws Exception
   {
      return createAnnotation(getRootExpr(annotationExpr, properties), null, cl);
   }
}
