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

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;

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

import org.jboss.aop.Advisor;
import org.jboss.aop.AspectManager;
import org.jboss.aop.annotation.AnnotationElement;
import org.jboss.aop.annotation.PortableAnnotationElement;
import org.jboss.aop.introduction.InterfaceIntroduction;
import org.jboss.aop.pointcut.ast.ASTAttribute;
import org.jboss.aop.pointcut.ast.ASTConstructor;
import org.jboss.aop.pointcut.ast.ASTException;
import org.jboss.aop.pointcut.ast.ASTField;
import org.jboss.aop.pointcut.ast.ASTMethod;
import org.jboss.aop.pointcut.ast.ASTParameter;
import org.jboss.aop.pointcut.ast.ClassExpression;
import org.jboss.aop.util.JavassistMethodHashing;
import org.jboss.aop.util.MethodHashing;

/**
 * Comment
 *
 * @author <a href="mailto:bill@jboss.org">Bill Burke</a>
 * @version $Revision: 1.37 $
 */
public class Util
{
   public static boolean matchesClassExpr(ClassExpression classExpr, CtClass clazz, Advisor advisor)
   {
      try
      {
         if (classExpr.isAnnotation())
         {
            String sub = classExpr.getOriginal().substring(1);
            if (advisor != null)
            {
               if (advisor.getClassMetaData().hasTag(sub)) return true;
               return advisor.hasAnnotation(clazz, sub);
            }
            else
            {
               return AnnotationElement.isAnyAnnotationPresent(clazz, sub);
            }
         }
         else if (classExpr.isInstanceOf())
         {
            return Util.subtypeOf(clazz, classExpr);
         }
         else if (classExpr.isTypedef())
         {
            return Util.matchesTypedef(clazz, classExpr, advisor);
         }
         else
         {
            return classExpr.matches(clazz.getName());
         }
      }
      catch (Exception e)
      {
         throw new RuntimeException(e);  //To change body of catch statement use Options | File Templates.
      }

   }

   public static boolean matchesClassExpr(ClassExpression classExpr, Class clazz)
   {
      return matchesClassExpr(classExpr, clazz, null);
   }
   
   public static boolean matchesClassExpr(ClassExpression classExpr, Class clazz, Advisor advisor)
   {
      try
      {
         if (classExpr.isAnnotation())
         {
            String sub = classExpr.getOriginal().substring(1);
            if (advisor != null)
            {
               if (advisor.getClassMetaData().hasTag(sub)) return true;
               return advisor.hasAnnotation(clazz, sub);
            }
            else
            {
               return AnnotationElement.isAnyAnnotationPresent(clazz, sub);
            }
         }
         else if (classExpr.isInstanceOf())
         {
            return Util.subtypeOf(clazz, classExpr, advisor);
         }
         else if (classExpr.isTypedef())
         {
            return Util.matchesTypedef(clazz, classExpr, advisor);
         }
         else
         {            
            return(classExpr.matches(clazz.getName()));
         }
      }
      catch (Exception e)
      {
         throw new RuntimeException(e);  //To change body of catch statement use Options | File Templates.
      }

   }

   /**
    * @param method Method we are looking for
    * @param target ClassExpression with the class/interface we are looking for the method in
    */
   public static boolean methodExistsInSuperClassOrInterface(Method method, ClassExpression target, boolean exactSuper) throws Exception
   {
      long hash = MethodHashing.calculateHash(method);
      boolean exists = methodExistsInSuperClassOrInterface(hash, target, method.getDeclaringClass(), exactSuper);
      return exists;
   }
   
   private static boolean methodExistsInSuperClassOrInterface(long hash, ClassExpression expr, Class clazz, boolean exactSuper) throws Exception
   {
      if (clazz == null) return false;
      
      if (expr.isAnnotation())
      {
         String sub = expr.getOriginal().substring(1);
          //System.out.print("Checking " + sub + "annotation in " + clazz.getName() + " ");
          if (AnnotationElement.isAnyAnnotationPresent(clazz, sub))
          {
             //System.out.println("Yo");
             if (classHasMethod(clazz, hash, exactSuper)) return true;
          }
          else
          {
             //System.out.println("Nah");
          }
       }
       else if (expr.matches(clazz.getName()))
       {
          if (classHasMethod(clazz, hash, exactSuper)) return true;
       }

       Class[] interfaces = clazz.getInterfaces();
       for (int i = 0; i < interfaces.length; i++)
       {
          if (methodExistsInSuperClassOrInterface(hash, expr, interfaces[i], exactSuper)) return true;
       }
       if (clazz.isInterface()) return false; // we are done
   
       return methodExistsInSuperClassOrInterface(hash, expr, clazz.getSuperclass(), exactSuper);
   }
   
   private static boolean classHasMethod(Class clazz, long hash, boolean exactSuper)throws Exception
   {
      Method m = MethodHashing.findMethodByHash(clazz, hash);
      if (m != null)
      {
         if (exactSuper)
         {
            //MethodHashing will check all super classes so make sure it is on the class itself
            return clazz == m.getDeclaringClass();
         }
         else
         {
            return true;
         }
      }
      
      return false;
   }

   /**
    * @param method CtMethod we are looking for
    * @param target ClassExpression with the class/interface we are looking for the method in
    */
   public static boolean methodExistsInSuperClassOrInterface(CtMethod method, ClassExpression target, boolean exactSuper) throws Exception
   {
      long hash = JavassistMethodHashing.methodHash(method);
      return methodExistsInSuperClassOrInterface(hash, target, method.getDeclaringClass(), exactSuper);
   }
   
   private static boolean methodExistsInSuperClassOrInterface(long hash, ClassExpression expr, CtClass clazz, boolean exactSuper) throws Exception
   {
      if (clazz == null) return false;
      
      if (expr.isAnnotation())
      {
         String sub = expr.getOriginal().substring(1);
         if (AnnotationElement.isAnyAnnotationPresent(clazz, sub))
         {
            if (classHasMethod(clazz, hash, exactSuper)) return true;
         }
      }
      else if (expr.matches(clazz.getName()))
      {
         if (classHasMethod(clazz, hash, exactSuper)) return true;
      }
      
      CtClass[] interfaces = clazz.getInterfaces();
      for (int i = 0; i < interfaces.length; i++)
      {
         if (methodExistsInSuperClassOrInterface(hash, expr, interfaces[i], exactSuper)) return true;
      }
      if (clazz.isInterface()) return false; // we are done
      
      return methodExistsInSuperClassOrInterface(hash, expr, clazz.getSuperclass(), exactSuper);
   }
   
   private static boolean classHasMethod(CtClass clazz, long hash, boolean exactSuper)throws Exception
   {
      HashMap methods = JavassistMethodHashing.getMethodMap(clazz);
      CtMethod m = (CtMethod)methods.get(new Long(hash));
      if (m != null)
      {
         if (exactSuper)
         {
            //If a class, JavassistMethodHashing will check all super classes so make sure it is on the class itself
            return clazz == m.getDeclaringClass();
         }
         else
         {
            return true;
         }
      }
      
      if (clazz.isInterface() && !exactSuper)
      {
         CtClass[] interfaces = clazz.getInterfaces();
         for (int i = 0 ; i < interfaces.length ; i++)
         {
            if (classHasMethod(interfaces[i], hash, exactSuper)) return true;
         }
      }
      return false;
   }
   
   public static boolean subtypeOf(CtClass clazz, ClassExpression instanceOf)
   {
      try
      {
	      if (clazz == null) return false;
	      if (instanceOf.isInstanceOfAnnotated())
	      {
            if (clazz.isPrimitive()) return false;
            String sub = instanceOf.getInstanceOfAnnotation().substring(1);
            if (PortableAnnotationElement.isAnyAnnotationPresent(clazz, sub)) return true;
	      }
	      else if (instanceOf.matches(clazz.getName()))
	      {
	         return true;
	      }
	      CtClass[] interfaces = clazz.getInterfaces();
	      for (int i = 0; i < interfaces.length; i++)
	      {
	         if (subtypeOf(interfaces[i], instanceOf)) return true;
	      }
	      if (clazz.isInterface()) return false; // we are done
	
	      return subtypeOf(clazz.getSuperclass(), instanceOf);
      } 
      catch (Exception e)
      {
         throw new RuntimeException(e);
      }
   }

   public static boolean subtypeOf(Class clazz, ClassExpression instanceOf, Advisor advisor)
   {
      return MatcherStrategy.getMatcher(advisor).subtypeOf(clazz, instanceOf, advisor);
   }
   
   public static boolean has(CtClass target, ASTMethod method, Advisor advisor)
   {
      return has(target, method, advisor, true);
   }
   
   public static boolean has(CtClass target, ASTMethod method, Advisor advisor, boolean checkSuper)
   {
      try
      {
         CtMethod[] methods = target.getDeclaredMethods();
         for (int i = 0; i < methods.length; i++)
         {
            MethodMatcher matcher = new MethodMatcher(advisor, methods[i], null);
            if (matcher.matches(method).booleanValue()) return true;
         }
         
         if (checkSuper)
         {
            CtClass superClass = target.getSuperclass();
            if (superClass != null) return has(superClass, method, advisor, checkSuper);
         }
      }
      catch (NotFoundException e)
      {
         throw new RuntimeException(e);  //To change body of catch statement use Options | File Templates.
      }
      return false;
   }

   public static boolean has(CtClass target, ASTField field, Advisor advisor)
   {
      return has(target, field, advisor, true);
   }
   
   public static boolean has(CtClass target, ASTField field, Advisor advisor, boolean checkSuper)
   {
      try
      {
         CtField[] fields = target.getDeclaredFields();
         for (int i = 0; i < fields.length; i++)
         {
            FieldGetMatcher matcher = new FieldGetMatcher(advisor, fields[i], null);
            if (((Boolean) field.jjtAccept(matcher, null)).booleanValue()) return true;
         }
         
         if (checkSuper)
         {
            CtClass superClass = target.getSuperclass();
            if (superClass != null) return has(superClass, field, advisor, checkSuper);
         }
      }
      catch (NotFoundException e)
      {
         throw new RuntimeException(e);  //To change body of catch statement use Options | File Templates.
      }
      return false;
   }

   public static boolean has(CtClass target, ASTConstructor con, Advisor advisor)
   {
      try
      {
         CtConstructor[] cons = target.getDeclaredConstructors();
         for (int i = 0; i < cons.length; i++)
         {
            ConstructorMatcher matcher = new ConstructorMatcher(advisor, cons[i], null);
            if (matcher.matches(con).booleanValue()) return true;
         }
      }
      catch (NotFoundException e)
      {
         throw new RuntimeException(e);  //To change body of catch statement use Options | File Templates.
      }
      return false;
   }

   public static boolean has(Class target, ASTMethod method, Advisor advisor)
   {
      return has(target, method, advisor, true);
   }
   
   public static boolean has(Class target, ASTMethod method, Advisor advisor, boolean checkSuper)
   {
      Method[] methods = advisor.getAllMethods();
      if (methods == null)
         methods = target.getDeclaredMethods();
      for (int i = 0; i < methods.length; i++)
      {
         MethodMatcher matcher = new MethodMatcher(advisor, methods[i], null);
         if (matcher.matches(method).booleanValue()) return true;
      }
      
      if (checkSuper)
      {
         Class superClass = target.getSuperclass();
         if (superClass != null) return has(superClass, method, advisor, checkSuper);
      }
      return false;
   }

   public static boolean has(Class target, ASTField field, Advisor advisor)
   {
      return has(target, field, advisor, true);
   }
   
   public static boolean has(Class target, ASTField field, Advisor advisor, boolean checkSuper)
   {
      Field[] fields = target.getDeclaredFields();
      for (int i = 0; i < fields.length; i++)
      {
         FieldGetMatcher matcher = new FieldGetMatcher(advisor, fields[i], null);
         if (((Boolean) field.jjtAccept(matcher, null)).booleanValue()) return true;
      }
      
      if (checkSuper)
      {
         Class superClass = target.getSuperclass();
         if (superClass != null) return has(superClass, field, advisor, checkSuper);
      }
      return false;
   }

   public static boolean has(Class target, ASTConstructor con, Advisor advisor)
   {
      Constructor[] cons = target.getDeclaredConstructors();
      for (int i = 0; i < cons.length; i++)
      {
         ConstructorMatcher matcher = new ConstructorMatcher(advisor, cons[i], null);
         if (matcher.matches(con).booleanValue()) return true;
      }
      return false;
   }

   public static boolean matchesTypedef(CtClass clazz, ClassExpression classExpr, Advisor advisor)
   {
      String original = classExpr.getOriginal();
      String typedefName = original.substring("$typedef{".length(), original.lastIndexOf("}"));
      AspectManager manager = (advisor != null) ? advisor.getManager() : AspectManager.instance(); 
      Typedef typedef = manager.getTypedef(typedefName);
      if (typedef == null) return false;
      return typedef.matches(advisor, clazz);
   }

   public static boolean matchesTypedef(Class clazz, ClassExpression classExpr, Advisor advisor)
   {
      String original = classExpr.getOriginal();
      String typedefName = original.substring("$typedef{".length(), original.lastIndexOf("}"));
      AspectManager manager = (advisor != null) ? advisor.getManager() : AspectManager.instance(); 
      Typedef typedef = manager.getTypedef(typedefName);
      if (typedef == null) return false;
      return typedef.matches(advisor, clazz);
   }

   public static boolean matchModifiers(ASTAttribute need, int have)
   {
      if (Modifier.isAbstract(need.attribute) && need.not) return !Modifier.isAbstract(have);
      if (Modifier.isAbstract(need.attribute) && !need.not) return Modifier.isAbstract(have);
      if (Modifier.isFinal(need.attribute) && need.not) return !Modifier.isFinal(have);
      if (Modifier.isFinal(need.attribute) && !need.not) return Modifier.isFinal(have);
      if (Modifier.isInterface(need.attribute) && need.not) return !Modifier.isInterface(have);
      if (Modifier.isInterface(need.attribute) && !need.not) return Modifier.isInterface(have);
      if (Modifier.isNative(need.attribute) && need.not) return !Modifier.isNative(have);
      if (Modifier.isNative(need.attribute) && !need.not) return Modifier.isNative(have);
      if (Modifier.isPrivate(need.attribute) && need.not) return !Modifier.isPrivate(have);
      if (Modifier.isPrivate(need.attribute) && !need.not) return Modifier.isPrivate(have);
      if (Modifier.isProtected(need.attribute) && need.not) return !Modifier.isProtected(have);
      if (Modifier.isProtected(need.attribute) && !need.not) return Modifier.isProtected(have);
      if (Modifier.isPublic(need.attribute) && need.not) return !Modifier.isPublic(have);
      if (Modifier.isPublic(need.attribute) && !need.not) return Modifier.isPublic(have);
      if (Modifier.isStatic(need.attribute) && need.not) return !Modifier.isStatic(have);
      if (Modifier.isStatic(need.attribute) && !need.not) return Modifier.isStatic(have);
      if (Modifier.isStrict(need.attribute) && need.not) return !Modifier.isStrict(have);
      if (Modifier.isStrict(need.attribute) && !need.not) return Modifier.isStrict(have);
      if (Modifier.isSynchronized(need.attribute) && need.not) return !Modifier.isSynchronized(have);
      if (Modifier.isSynchronized(need.attribute) && !need.not) return Modifier.isSynchronized(have);
      if (Modifier.isTransient(need.attribute) && need.not) return !Modifier.isTransient(have);
      if (Modifier.isTransient(need.attribute) && !need.not) return Modifier.isTransient(have);
      if (Modifier.isVolatile(need.attribute) && need.not) return !Modifier.isVolatile(have);
      if (Modifier.isVolatile(need.attribute) && !need.not) return Modifier.isVolatile(have);

      return true;
   }

   /**
    * @param nodeExceptions  ArrayList of ASTException entries for a given ASTMethod or ASTConstructor
    * @param foundExceptions Array of Exceptions found for a method/constructor
    */
   public static boolean matchExceptions(ArrayList nodeExceptions, CtClass[] foundExceptions)
   {
      if (nodeExceptions.size() > foundExceptions.length) return false;
      for (Iterator it = nodeExceptions.iterator(); it.hasNext();)
      {
         boolean found = false;
         ASTException ex = (ASTException) it.next();
         for (int i = 0; i < foundExceptions.length; i++)
         {
            if (ex.getType().matches(foundExceptions[i].getName()))
            {
               found = true;
               break;
            }
         }

         if (!found) return false;
      }

      return true;
   }

   /**
    * @param nodeExceptions  ArrayList of ASTException entries for a given ASTMethod or ASTConstructor
    * @param foundExceptions Array of Exceptions found for a method/constructor
    */
   public static boolean matchExceptions(ArrayList nodeExceptions, Class[] foundExceptions)
   {
      if (nodeExceptions.size() > foundExceptions.length) return false;
      for (Iterator it = nodeExceptions.iterator(); it.hasNext();)
      {
         boolean found = false;
         ASTException ex = (ASTException) it.next();
         for (int i = 0; i < foundExceptions.length; i++)
         {
            if (ex.getType().matches(foundExceptions[i].getName()))
            {
               found = true;
               break;
            }
         }

         if (!found) return false;
      }

      return true;
   }

   public static boolean matchesParameters(Advisor advisor, ASTMethod node, CtMethod ctMethod)
   {
      if (node.isAnyParameters()) return true;
      try
      {
         return Util.matchesParameters(advisor, node.hasAnyZeroOrMoreParameters(), node.getParameters(), ctMethod.getParameterTypes());
      }
      catch (NotFoundException e)
      {
         // AutoGenerated
         throw new RuntimeException(e);
      }
   }

   public static boolean matchesParameters(Advisor advisor, ASTConstructor node, CtConstructor ctConstructor)
   {
      int i = 0;
      if (node.isAnyParameters()) return true;
      try
      {
         CtClass[] params = ctConstructor.getParameterTypes();
         return Util.matchesParameters(advisor, node.hasAnyZeroOrMoreParameters(), node.getParameters(), params);
      }
      catch (NotFoundException e)
      {
         // AutoGenerated
         throw new RuntimeException(e);
      }
   }

   public static boolean matchesParameters(Advisor advisor, ASTMethod node, Method method) 
   {
      if (node.isAnyParameters()) return true;
      return matchesParameters(advisor, node.hasAnyZeroOrMoreParameters(), node.getParameters(), method.getParameterTypes());
   }

   public static boolean matchesParameters(Advisor advisor, ASTConstructor node, Constructor con) 
   {
      if (node.isAnyParameters()) return true;
      
      Class[] params = con.getParameterTypes();
      
      return matchesParameters(advisor, node.hasAnyZeroOrMoreParameters(), node.getParameters(), con.getParameterTypes());
   }

   private static boolean matchesParameters(Advisor advisor, boolean hasAnyZeroOrMoreParameters, ArrayList parameters, Class[] params)
   {
      RefParameterMatcher matcher = new RefParameterMatcher(advisor, parameters, params);
      return matcher.matches();
   }

   private static boolean matchesParameters(Advisor advisor, boolean hasAnyZeroOrMoreParameters, ArrayList parameters, CtClass[] params)
   {
      CtParameterMatcher matcher = new CtParameterMatcher(advisor, parameters, params);
      return matcher.matches();
   }
   
   private static class ParameterMatcher
   {
      Advisor advisor;
      ArrayList astParameters;
      final long paramsLength;
      int asti;
      int actuali;
      
      ParameterMatcher(Advisor advisor, ArrayList parameters, Object[] params)
      {
         this.advisor = advisor;
         this.astParameters = parameters;
         paramsLength = params.length;
      }

      boolean matches()
      {
         return matches(0, 0);
      }
      
      private boolean matches(int ast, int actual)
      {
         boolean match = true;
         for ( ; match && ast < astParameters.size() && actual < paramsLength  ; ast++)
         {
            if (isAnyZeroOrMoreParameters(ast))
            {
               asti = ast;
               actuali = actual;
               match = wildcard();//ast, cls);
               ast = asti;
               actual = actuali;
               ast--;
            }
            else
            {
               match = doMatch(ast, actual);
               actual++;
            }
         }
         
         while (match && ast < astParameters.size() && isAnyZeroOrMoreParameters(ast))
         {
            ast++;
         }
         return (match && ast == astParameters.size() && paramsLength == actual);
      }
      
      private boolean isAnyZeroOrMoreParameters(int index)
      {
         return ((ASTParameter)astParameters.get(index)).isAnyZeroOrMoreParameters();
      }
      
      boolean doMatch(int astIndex, int actualIndex)
      {
         //Overridden by sub classes
         return false;
      }
      
      private boolean wildcard()
      {
         boolean match = true;
         asti++;
         
         while (actuali < paramsLength && asti < astParameters.size() && isAnyZeroOrMoreParameters(asti))
         {
            asti++;
         }

         while (asti < astParameters.size() && isAnyZeroOrMoreParameters(asti))
         {
            asti++;
         }

         if (actuali == paramsLength && asti < astParameters.size())
            return false;
         if (actuali == paramsLength && asti == astParameters.size())
            return true; 
         else
         {
            if (!matches(asti, actuali))
            {
               do
               {
                  actuali++;
                  while (actuali < paramsLength && asti < astParameters.size() && !doMatch(asti, actuali))
                  {
                     actuali++;
                  }
                  
               }while ( (actuali < paramsLength) ? !(match = matches(asti, actuali)) : false);
               
            }
            if (actuali == paramsLength && asti == astParameters.size())
            {
               match = true;
            }
            return match;
         }
      }
   }
   
   private static class RefParameterMatcher extends ParameterMatcher
   {
      Class[] params;
      public RefParameterMatcher(Advisor advisor, ArrayList parameters, Class[] params)
      {
         super(advisor, parameters, params);
         this.params = params;
      }
      
      boolean doMatch(int astIndex, int actualIndex)
      {
         ASTParameter ast = (ASTParameter) astParameters.get(astIndex);
         ClassExpression exp = ast.getType();

         if (exp.isSimple())
         {
            String asString = ClassExpression.simpleType(params[actualIndex]);
            if (!exp.matches(asString)) return false;
         }
         else
         {
            if (!Util.matchesClassExpr(exp, params[actualIndex], advisor)) return false;
         }
         
         return true;
      }
   }

   private static class CtParameterMatcher extends ParameterMatcher
   {
      CtClass[] params;
      public CtParameterMatcher(Advisor advisor, ArrayList parameters, CtClass[] params)
      {
         super(advisor, parameters, params);
         this.params = params;
      }
      
      boolean doMatch(int astIndex, int actualIndex)
      {
         ASTParameter ast = (ASTParameter) astParameters.get(astIndex);
         ClassExpression exp = ast.getType();
         if (!matchesClassExpr(exp, params[actualIndex], advisor)) return false;
         return true;
      }
   }
}
