/*
* 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.metadata.spi.signature;

import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.util.Arrays;

import org.jboss.metadata.plugins.signature.SignatureNameUtil;
import org.jboss.reflect.spi.ConstructorInfo;
import org.jboss.reflect.spi.FieldInfo;
import org.jboss.reflect.spi.MemberInfo;
import org.jboss.reflect.spi.MethodInfo;
import org.jboss.reflect.spi.PrimitiveInfo;
import org.jboss.reflect.spi.TypeInfo;

/**
 * Signature.
 * 
 * @author <a href="adrian@jboss.com">Adrian Brock</a>
 * @version $Revision: 1.1 $
 */
public class Signature
{
   /** No Name */
   public static final String NO_NAME = "?%NO_NAME%?";

   /** No Parameters */
   public static final String[] NO_PARAMETERS = new String[0];

   /** No Parameters Types */
   public static final Class<?>[] NO_PARAMETER_TYPES = new Class[0];
   
   /** The name */
   private String name;
   
   /** The parameters */
   private String[] parameters;

   /** The parameter types */
   private Class<?>[] parameterTypes;
   
   /** The cached hashcode */
   private transient int cachedHashCode = Integer.MIN_VALUE;

   /**
    * Get a signature for a member
    * 
    * @param member the member
    * @return the result
    */
   public static Signature getSignature(Member member)
   {
      if (member == null)
         throw new IllegalArgumentException("Null member");
      
      if (member instanceof Method)
      {
         Method method = Method.class.cast(member);
         return new MethodSignature(method);
      }
      if (member instanceof Field)
      {
         Field field = Field.class.cast(member);
         return new FieldSignature(field);
      }
      if (member instanceof Constructor)
      {
         Constructor<?> constructor = Constructor.class.cast(member);
         return new ConstructorSignature(constructor);
      }
      throw new IllegalArgumentException("Unknown member: " + member);
   }

   /**
    * Get a signature for a member info
    * 
    * @param member the member
    * @return the result
    */
   public static Signature getSignature(MemberInfo member)
   {
      if (member == null)
         throw new IllegalArgumentException("Null member");
      
      if (member instanceof MethodInfo)
      {
         MethodInfo method = MethodInfo.class.cast(member);
         return new MethodSignature(method);
      }
      if (member instanceof FieldInfo)
      {
         FieldInfo field = FieldInfo.class.cast(member);
         return new FieldSignature(field);
      }
      if (member instanceof ConstructorInfo)
      {
         ConstructorInfo constructor = ConstructorInfo.class.cast(member);
         return new ConstructorSignature(constructor);
      }
      throw new IllegalArgumentException("Unknown member: " + member);
   }
   
   @Deprecated // Use PrimitiveInfo.getPrimitiveArrayType(String)
   public static String getPrimativeArrayType(String name)
   {
      return PrimitiveInfo.getPrimativeArrayType(name);
   }

   public static String[] convertParameters(TypeInfo[] typeInfos)
   {
      if (typeInfos == null || typeInfos.length == 0)
         return NO_PARAMETERS;
      
      String[] paramTypes = new String[typeInfos.length];
      for (int i = 0; i < typeInfos.length; ++i)
         paramTypes[i] = typeInfos[i].getName();
      return paramTypes;
   }

   @SuppressWarnings("deprecation")
   public static Class<?>[] convertParameterTypes(TypeInfo[] typeInfos)
   {
      if (typeInfos == null || typeInfos.length == 0)
         return NO_PARAMETER_TYPES;

      Class<?>[] paramTypes = new Class<?>[typeInfos.length];
      for (int i = 0; i < typeInfos.length; ++i)
         paramTypes[i] = typeInfos[i].getType();
      return paramTypes;
   }

   /**
    * Convert classes to string
    * 
    * @param parameters the parameters as classes
    * @return the parameters as strings
    */
   private static String[] classesToStrings(Class<?>... parameters)
   {
      if (parameters == null || parameters.length == 0)
         return NO_PARAMETERS;

      String[] result = new String[parameters.length];
      for (int i = 0; i < result.length; ++i)
      {
         if (parameters[i] == null)
            throw new IllegalArgumentException("Null class");
         result[i] = parameters[i].getName();
      }
      return result;
   }

   /**
    * Convert classes to string
    * 
    * @param clazz the reference class
    * @param parameters the parameters as strings
    * @return the parameters as classes
    */
   private static Class<?>[] stringsToClasses(Class<?> clazz, String... parameters)
   {
      if (clazz == null)
         throw new IllegalArgumentException("Null clazz");
      
      ClassLoader cl = clazz.getClassLoader();
      if (cl == null)
      {
         cl = Class.class.getClassLoader();
         if (cl == null)
         {
            //Bootstrap classloader was represented as null, use the tcl
            cl = Thread.currentThread().getContextClassLoader();
         }
      }

      return stringsToClasses(cl, parameters);
   }

   /**
    * Loads a class by name.
    * 
    * TODO A similar method exists in org.jboss.util.Classes ...
    */
   private static Class<?> loadClass(ClassLoader cl, String name) throws ClassNotFoundException
   {
      Class<?> primitive = PrimitiveInfo.getPrimativeType(name);
      if (primitive != null)
      {
         return primitive;
      }

      int index = name.lastIndexOf('[');
      if (index >= 0)
      {
         // count opening braces
         int arrayDimension = 0;
         while (name.charAt(arrayDimension) == '[')
            arrayDimension++;
         Class<?> componentType;
         if (name.charAt(arrayDimension) == 'L')
         {
            String arrayType = name.substring(arrayDimension + 1, name.length() - 1);
            componentType = loadClass(cl, arrayType);
         }
         else
         {
            String arrayType = name.substring(arrayDimension);
            componentType = PrimitiveInfo.getPrimativeArrayComponentType(arrayType);
         }

         // construct array class
         return Array.newInstance(componentType, new int[arrayDimension]).getClass();
      }
      return Class.forName(name, true, cl);
   }
   
   /**
    * Convert classes to string
    * 
    * @param cl the classloader
    * @param parameters the parameters as strings
    * @return the parameters as classes
    */
   private static Class<?>[] stringsToClasses(ClassLoader cl, String... parameters)
   {
      if (cl == null)
         throw new IllegalArgumentException("Null classloader");

      if (parameters == null || parameters.length == 0)
         return NO_PARAMETER_TYPES;

      Class<?>[] result = new Class[parameters.length];
      for (int i = 0; i < result.length; ++i)
      {
         try
         {
            result[i] = loadClass(cl, parameters[i]);
         }
         catch (ClassNotFoundException e)
         {
            throw new IllegalStateException("Class not found: " + parameters[i], e);
         }
      }
      return result;
   }
   
   /**
    * Create a new Signature.
    */
   public Signature()
   {
      this(NO_NAME, NO_PARAMETER_TYPES, NO_PARAMETERS);
   }
   
   /**
    * Create a new Signature.
    * 
    * @param name the  name
    */
   public Signature(String name)
   {
      this(name, null, NO_PARAMETERS);
   }

   /**
    * Create a new Signature.
    * 
    * @param parameters the parameters
    */
   public Signature(String... parameters)
   {
      this(NO_NAME, null, parameters);
   }

   /**
    * Create a new Signature.
    * 
    * @param parameters the parameters
    */
   public Signature(Class<?>... parameters)
   {
      this(NO_NAME, parameters, null);
   }

   /**
    * Create a new Signature.
    * 
    * @param name the  name
    * @param parameters the parameters
    */
   public Signature(String name, Class<?>... parameters)
   {
      this(name, parameters, null);
   }

   /**
    * Create a new Signature.
    * 
    * @param name the  name
    * @param parameters the parameters
    */
   public Signature(String name, String... parameters)
   {
      this(name, null, parameters);
   }

   /**
    * Create a new Signature.
    * 
    * @param name the  name
    * @param parameterTypes the parameterTypes
    * @param parameters the parameters
    */
   private Signature(String name, Class<?>[] parameterTypes, String[] parameters)
   {
      this.name = name;
      if (name == null)
         this.name = NO_NAME;
      this.parameters = parameters;
      this.parameterTypes = parameterTypes;
   }
   
   /**
    * Returns the name.
    * 
    * @return the name.
    */
   public String getName()
   {
      return name;
   }

   /**
    * Returns the parameters.
    * 
    * @return the parameters.
    */
   public String[] getParameters()
   {
      if (parameters == null)
         parameters = classesToStrings(getParameterTypes());
      return parameters;
   }
   
   /**
    * Get the parameter types (for override by sub-classes)
    * 
    * @return the parameters types
    */
   protected Class<?>[] getParameterTypes()
   {
      return parameterTypes;
   }
   
   /**
    * Returns the parameter types.
    * 
    * @param clazz the reference class
    * @return the parameter types.
    */
   public Class<?>[] getParametersTypes(Class<?> clazz)
   {
      if (parameterTypes == null)
      {
         parameterTypes = getParameterTypes();
         if (parameterTypes == null)
            parameterTypes = stringsToClasses(clazz, parameters);
      }
      return parameterTypes;
   }
   
   public boolean equals(Object obj)
   {
      if (obj == this)
         return true;
      if (obj == null || obj instanceof Signature == false)
         return false;
      
      Signature other = (Signature) obj;
      
      if (getName().equals(other.getName()) == false)
         return false;
      
      String[] thisParameters = getParameters();
      String[] otherParameters = other.getParameters();
      
      if (thisParameters.length != otherParameters.length)
         return false;
      
      for (int i = 0; i < thisParameters.length; ++i)
      {
         if (thisParameters[i].equals(otherParameters[i]) == false)
            return false;
      }
      return true;
   }
   
   public int hashCode()
   {
      if (cachedHashCode == Integer.MIN_VALUE)
         cachedHashCode = toString().hashCode();
      return cachedHashCode;
   }
   
   public String toString()
   {
      StringBuilder builder = new StringBuilder();
      internalToString(builder);
      return builder.toString();
   }

   /**
    * Build the to String
    * 
    * @param builder the builder to use
    */
   protected void internalToString(StringBuilder builder)
   {
      SignatureNameUtil.signatureToString(builder, name, getParameters());
   }
}
