/*
* JBoss, Home of Professional Open Source
* Copyright 2006, 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.reflect.plugins.javassist;

import java.lang.reflect.Array;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtField;
import javassist.CtMethod;
import javassist.NotFoundException;

import org.jboss.reflect.plugins.PackageInfoImpl;
import org.jboss.reflect.plugins.TypeInfoAttachments;
import org.jboss.reflect.plugins.ValueConvertor;
import org.jboss.reflect.spi.AnnotationValue;
import org.jboss.reflect.spi.ClassInfo;
import org.jboss.reflect.spi.ConstructorInfo;
import org.jboss.reflect.spi.FieldInfo;
import org.jboss.reflect.spi.InterfaceInfo;
import org.jboss.reflect.spi.MethodInfo;
import org.jboss.reflect.spi.PackageInfo;
import org.jboss.reflect.spi.TypeInfo;
import org.jboss.reflect.spi.TypeInfoFactory;
import org.jboss.util.JBossStringBuilder;

/**
 * JavassistTypeInfo.
 * 
 * @author <a href="adrian@jboss.com">Adrian Brock</a>
 * @version $Revision: 81545 $
 */
public class JavassistTypeInfo extends JavassistInheritableAnnotationHolder implements ClassInfo, InterfaceInfo
{
   /** The serialVersionUID */
   private static final long serialVersionUID = -5072033691434335775L;

   /** The factory */
   private JavassistTypeInfoFactoryImpl factory;

   /** The name */
   private String name;
   
   /** The class */
   private Class<? extends Object> clazz;

   /** The constructors */
   private Map<SignatureKey, JavassistConstructorInfo> constructors = new ConcurrentHashMap<SignatureKey, JavassistConstructorInfo>();

   /** The constructors */
   private ConstructorInfo[] constructorArray;

   /** The fields */
   private Map<String, JavassistFieldInfo> fields = new ConcurrentHashMap<String, JavassistFieldInfo>();

   /** The fields */
   private FieldInfo[] fieldArray;

   /** The methods */
   private Map<SignatureKey, JavassistMethodInfo> methods = new ConcurrentHashMap<SignatureKey, JavassistMethodInfo>();

   /** The methods */
   private MethodInfo[] methodArray;

   /** The package info */
   private PackageInfo packageInfo;
   
   /** The attachments */
   private transient TypeInfoAttachments attachments;

   /**
    * Create a new JavassistTypeInfo.
    * 
    * @param factory the factory
    * @param ctClass the ctClass
    * @param clazz the class
    */
   JavassistTypeInfo(JavassistTypeInfoFactoryImpl factory, CtClass ctClass, Class<? extends Object> clazz)
   {
      this(factory, ctClass.getName(), ctClass, clazz);
   }

   /**
    * Create a new JavassistTypeInfo.
    * 
    * @param factory the factory
    * @param ctClass the ctClass
    * @param clazz the class
    * @param name the name
    */
   JavassistTypeInfo(JavassistTypeInfoFactoryImpl factory, String name, CtClass ctClass, Class<? extends Object> clazz)
   {
      super(ctClass, factory);
      this.factory = factory;
      this.clazz = clazz;
      this.name = name;
   }

   public String getName()
   {
      return name;
   }

   public boolean isInterface()
   {
      return ctClass.isInterface();
   }

   public String getSimpleName()
   {
      return getType().getSimpleName();
   }

   public int getModifiers()
   {
      return ctClass.getModifiers();
   }

   public boolean isPublic()
   {
      return Modifier.isPublic(getModifiers());
   }

   public boolean isStatic()
   {
      return Modifier.isStatic(getModifiers());
   }

   public boolean isVolatile()
   {
      return Modifier.isVolatile(getModifiers());
   }

   @Deprecated
   public Class<? extends Object> getType()
   {
      return clazz;
   }

   public ClassInfo getSuperclass()
   {
      if (isInterface())
         return null;
      try
      {
         CtClass superclass = ctClass.getSuperclass();
         if (superclass == null)
            return null;
         return (ClassInfo) factory.getTypeInfo(superclass);
      }
      catch (NotFoundException e)
      {
         throw JavassistTypeInfoFactoryImpl.raiseClassNotFound(clazz.getSuperclass().getName(), e);
      }
   }

   public ClassInfo getGenericSuperclass()
   {
      // TODO JBMICROCONT-129 getGenericSuperclass
      throw new org.jboss.util.NotImplementedException("getGenericSuperclass");
   }

   public InterfaceInfo[] getInterfaces()
   {
      try
      {
         CtClass[] interfaces = ctClass.getInterfaces();
         if (interfaces == null || interfaces.length == 0)
            return null;
         InterfaceInfo[] result = new InterfaceInfo[interfaces.length];
         for (int i = 0; i < result.length; ++i)
            result[i] = (InterfaceInfo) factory.getTypeInfo(interfaces[i]);
         return result;
      }
      catch (NotFoundException e)
      {
         throw JavassistTypeInfoFactoryImpl.raiseClassNotFound("for interfaces of " + getName(), e);
      }
   }

   public InterfaceInfo[] getGenericInterfaces()
   {
      // TODO JBMICROCONT-129 getGenericInterfaces
      throw new org.jboss.util.NotImplementedException("getGenericInterfaces");
   }

   public ConstructorInfo[] getDeclaredConstructors()
   {
      if (constructorArray == null)
      {
         CtConstructor[] declaredConstructors = ctClass.getDeclaredConstructors();
         if (declaredConstructors == null || declaredConstructors.length == 0)
            constructorArray = new ConstructorInfo[0];
         else
         {
            synchronized (constructors)
            {
               for (int i = 0; i < declaredConstructors.length; ++i)
                  generateConstructorInfo(declaredConstructors[i]);
               Collection<JavassistConstructorInfo> constructorCollection = constructors.values();
               constructorArray = constructorCollection.toArray(new ConstructorInfo[constructorCollection.size()]);
            }
         }
      }
      return constructorArray;
   }

   public ConstructorInfo getDeclaredConstructor(TypeInfo[] parameters)
   {
      SignatureKey key = new SignatureKey(null, parameters);
      synchronized (constructors)
      {
         ConstructorInfo constructor = constructors.get(key);
         if (constructor != null)
            return constructor;
      }
      if (constructorArray != null)
         return null;
      return generateConstructorInfo(key);
   }

   public FieldInfo getDeclaredField(String name)
   {
      synchronized (fields)
      {
         FieldInfo field = fields.get(name);
         if (field != null)
            return field;
      }
      if (fieldArray != null)
         return null;
      try
      {
         CtField field = ctClass.getDeclaredField(name);
         if (field == null)
            return null;
         return generateFieldInfo(field);
      }
      catch (NotFoundException e)
      {
         return null;
      }
   }

   public FieldInfo[] getDeclaredFields()
   {
      if (fieldArray == null)
      {
         CtField[] declaredFields = ctClass.getDeclaredFields();
         if (declaredFields == null || declaredFields.length == 0)
            fieldArray = new FieldInfo[0];
         else
         {
            synchronized (fields)
            {
               for (int i = 0; i < declaredFields.length; ++i)
                  generateFieldInfo(declaredFields[i]);
               Collection<JavassistFieldInfo> fieldCollection = fields.values();
               fieldArray = fieldCollection.toArray(new FieldInfo[fieldCollection.size()]);
            }
         }
      }
      return fieldArray;
   }

   public MethodInfo getDeclaredMethod(String name, TypeInfo[] parameters)
   {
      SignatureKey key = new SignatureKey(name, parameters);
      synchronized (methods)
      {
         MethodInfo method = methods.get(key);
         if (method != null)
            return method;
      }
      if (methodArray != null)
         return null;
      return generateMethodInfo(key);
   }

   public MethodInfo[] getDeclaredMethods()
   {
      if (methodArray == null)
      {
         CtMethod[] declaredMethods = ctClass.getDeclaredMethods();
         if (declaredMethods == null || declaredMethods.length == 0)
            methodArray = new MethodInfo[0];
         else
         {
            synchronized (methods)
            {
               for (int i = 0; i < declaredMethods.length; ++i)
                  generateMethodInfo(declaredMethods[i]);
               Collection<JavassistMethodInfo> methodCollection = methods.values();
               methodArray = methodCollection.toArray(new MethodInfo[methodCollection.size()]);
            }
         }
      }
      return methodArray;
   }

   public boolean isArray()
   {
      return getType().isArray();
   }

   public boolean isCollection()
   {
      return Collection.class.isAssignableFrom(getType());
   }

   public boolean isMap()
   {
      return Map.class.isAssignableFrom(getType());
   }

   public boolean isAnnotation()
   {
      return getType().isAnnotation();
   }

   public boolean isEnum()
   {
      return getType().isEnum();
   }

   public boolean isPrimitive()
   {
      return getType().isPrimitive();
   }

   /**
    * Get an array class
    * 
    * @param clazz the class
    * @return the array class
    */
   public static Class<?> getArrayClass(Class<?> clazz)
   {
      return Array.newInstance(clazz, 0).getClass();
   }

   public TypeInfo getArrayType()
   {
      Class<?> arrayClass = getArrayClass(getType());
      return factory.getTypeInfo(arrayClass);
   }

   @SuppressWarnings("deprecation")
   public Object newArrayInstance(int size) throws Throwable
   {
      if (isArray() == false)
         throw new ClassCastException(this + " is not an array.");
      return Array.newInstance(getComponentType().getType(), size);
   }

   @SuppressWarnings("deprecation")
   public boolean isAssignableFrom(TypeInfo info)
   {
      if (info == null)
         throw new NullPointerException("Parameter info cannot be null!");

      return getType().isAssignableFrom(info.getType());
   }

   @SuppressWarnings("deprecation")
   public boolean isInstance(Object object)
   {
      return getType().isInstance(object);
   }

   public TypeInfoFactory getTypeInfoFactory()
   {
      return factory;
   }

   public Object convertValue(Object value) throws Throwable
   {
      return ValueConvertor.convertValue(getType(), value);
   }

   public Object convertValue(Object value, boolean replaceProperties) throws Throwable
   {
      return ValueConvertor.convertValue(getType(), value, replaceProperties);
   }

   public Object convertValue(Object value, boolean replaceProperties, boolean trim) throws Throwable
   {
      return ValueConvertor.convertValue(getType(), value, replaceProperties, trim);
   }

   protected int getHashCode()
   {
      return getName().hashCode();
   }

   public boolean equals(Object obj)
   {
      if (obj == this)
         return true;
      if (obj == null || obj instanceof TypeInfo == false)
         return false;

      TypeInfo other = (TypeInfo) obj;
      return getName().equals(other.getName());
   }

   public void toShortString(JBossStringBuilder buffer)
   {
      buffer.append(getName());
   }

   protected void toString(JBossStringBuilder buffer)
   {
      buffer.append("name=").append(getName());
      super.toString(buffer);
   }

   /**
    * Get the factory
    * 
    * @return the factory
    */
   protected JavassistTypeInfoFactoryImpl getFactory()
   {
      return factory;
   }

   /**
    * Generate constructor info
    * 
    * @param constructor the constructor
    * @return the constructor info
    */
   protected ConstructorInfo generateConstructorInfo(CtConstructor constructor)
   {
      try
      {
         CtClass[] parameterTypes = constructor.getParameterTypes();
         String[] params = new String[parameterTypes.length];
         for (int i = 0; i < params.length; ++i)
            params[i] = parameterTypes[i].getName();
         SignatureKey key = new SignatureKey(null, params);
         JavassistConstructorInfo info = new JavassistConstructorInfo(factory, this, constructor);
         synchronized (constructors)
         {
            constructors.put(key, info);
         }
         return info;
      }
      catch (NotFoundException e)
      {
         throw JavassistTypeInfoFactoryImpl.raiseClassNotFound("for constructor of " + getName(), e);
      }
   }

   /**
    * Generate constructor info
    * 
    * @param key the key
    * @return the constructor info
    */
   protected ConstructorInfo generateConstructorInfo(SignatureKey key)
   {
      CtClass[] params = getParameterTypes(key);
      try
      {
         CtConstructor ctConstructor = ctClass.getDeclaredConstructor(params);
         return generateConstructorInfo(ctConstructor);
      }
      catch (NotFoundException e)
      {
         throw JavassistTypeInfoFactoryImpl.raiseMethodNotFound("for constructor " + getName(), e);
      }
   }

   /**
    * Generate field info
    * 
    * @param field the field
    * @return the field info
    */
   protected FieldInfo generateFieldInfo(CtField field)
   {
      JavassistFieldInfo info = new JavassistFieldInfo(factory, this, field);
      synchronized (fields)
      {
         fields.put(field.getName(), info);
      }
      return info;
   }

   /**
    * Generate method info
    * 
    * @param key the key
    * @return the method info
    */
   protected MethodInfo generateMethodInfo(SignatureKey key)
   {
      CtClass[] params = getParameterTypes(key);
      try
      {
         CtMethod ctMethod = ctClass.getDeclaredMethod(key.name, params);
         return generateMethodInfo(key, ctMethod);
      }
      catch (NotFoundException e)
      {
         throw JavassistTypeInfoFactoryImpl.raiseMethodNotFound("for method " + key.name, e);
      }
   }

   /**
    * Generate method info
    * 
    * @param method the method
    * @return the method info
    */
   protected MethodInfo generateMethodInfo(CtMethod method)
   {
      try
      {
         CtClass[] parameterTypes = method.getParameterTypes();
         String[] params = new String[parameterTypes.length];
         for (int i = 0; i < params.length; ++i)
            params[i] = parameterTypes[i].getName();
         SignatureKey key = new SignatureKey(method.getName(), params);
         return generateMethodInfo(key, method);
      }
      catch (NotFoundException e)
      {
         throw JavassistTypeInfoFactoryImpl.raiseClassNotFound("for method " + method.getName(), e);
      }
   }

   /**
    * Generate method info
    * 
    * @param key the key
    * @param method the method
    * @return the method info
    */
   protected MethodInfo generateMethodInfo(SignatureKey key, CtMethod method)
   {
      JavassistMethodInfo info = new JavassistMethodInfo(factory, this, key, method);
      synchronized (methods)
      {
         methods.put(key, info);
      }
      return info;
   }

   /**
    * Get the parameter types
    * 
    * @param key the key
    * @return the parameter types
    */
   protected CtClass[] getParameterTypes(SignatureKey key)
   {
      if (key.params == null)
         return null;

      CtClass[] result = new CtClass[key.params.length];
      for (int i = 0; i < key.params.length; ++i)
         result[i] = factory.getCtClass(key.params[i]);

      return result;
   }

   protected Object getAnnotatedTarget()
   {
      return ctClass;
   }
   
   public AnnotationValue[] getAnnotations()
   {
      return getAnnotations(ctClass);
   }

   @Override
   public JavassistInheritableAnnotationHolder getSuperHolder()
   {
      try
      {
         CtClass zuper = ctClass.getSuperclass();
         if (zuper == null)
         {
            return null;
         }
         return (JavassistTypeInfo)factory.getTypeInfo(zuper);
      }
      catch (NotFoundException e)
      {
         throw new RuntimeException(e);
      }
   }

   public TypeInfo[] getActualTypeArguments()
   {
      return null;
   }

   public TypeInfo getOwnerType()
   {
      return null;
   }

   public ClassInfo getRawType()
   {
      return this;
   }

   public TypeInfo getComponentType()
   {
      return null;
   }

   public TypeInfo getKeyType()
   {
      return null;
   }

   public TypeInfo getValueType()
   {
      return null;
   }

   public PackageInfo getPackage()
   {
      if (packageInfo == null)
      {
         String name = ctClass.getPackageName();
         if (name != null)
            packageInfo = new PackageInfoImpl(ctClass.getPackageName());
      }
      // TODO package annotations
      return packageInfo;
   }

   public void setAttachment(String name, Object attachment)
   {
      if (name == null)
         throw new IllegalArgumentException("Null name");
      synchronized (this)
      {
         if (attachments == null)
         {
            if (attachment == null)
               return;
            attachments = new TypeInfoAttachments();;
         }
      }
      if (attachment == null)
         attachments.removeAttachment(name);
      else
         attachments.addAttachment(name, attachment);
   }

   public <T> T getAttachment(Class<T> expectedType)
   {
      if (expectedType == null)
         throw new IllegalArgumentException("Null expectedType");
      Object result = getAttachment(expectedType.getName());
      if (result == null)
         return null;
      return expectedType.cast(result);
   }

   public Object getAttachment(String name)
   {
      if (name == null)
         throw new IllegalArgumentException("Null name");
      synchronized (this)
      {
         if (attachments == null)
            return null;
      }
      return attachments.getAttachment(name);
   }
}
