/*
 * Copyright (C) The MX4J Contributors.
 * All rights reserved.
 *
 * This software is distributed under the terms of the MX4J License version 1.0.
 * See the terms of the MX4J License in the documentation provided with this software.
 */

package test.javax.management.compliance.signature.support;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

/**
 * @version $Revision: 1.5 $
 */
public class SignatureVerifier
{
   public void verifySignature(String className, ClassLoader jmxriLoader, ClassLoader mx4jLoader) throws Exception
   {
      Class jmxriClass = jmxriLoader.loadClass(className);
      Class mx4jClass = mx4jLoader.loadClass(className);

      int modifiers = jmxriClass.getModifiers();
      boolean isPublic = Modifier.isPublic(modifiers);
      boolean isProtected = Modifier.isProtected(modifiers);
      boolean isPackage = !Modifier.isPrivate(modifiers) && !isProtected && !isPublic;
      boolean isSerializable = Serializable.class.isAssignableFrom(jmxriClass);

      NotCompliantWarningException warning = null;

      try
      {
         checkSameClassModifiers(jmxriClass, mx4jClass);
      }
      catch (NotCompliantWarningException x)
      {
         warning = x;
      }

      try
      {
         checkSameInheritance(jmxriClass, mx4jClass);
      }
      catch (NotCompliantWarningException x)
      {
         warning = x;
      }

      if (!isPackage)
      {
         try
         {
            checkSameConstructors(jmxriClass, mx4jClass);
         }
         catch (NotCompliantWarningException x)
         {
            warning = x;
         }
         try
         {
            checkSameMethods(jmxriClass, mx4jClass);
         }
         catch (NotCompliantWarningException x)
         {
            warning = x;
         }
         try
         {
            checkSameFields(jmxriClass, mx4jClass);
         }
         catch (NotCompliantWarningException x)
         {
            warning = x;
         }
      }

      if (isSerializable)
      {
         try
         {
            checkSameSerialVersionUID(jmxriClass, mx4jClass);
         }
         catch (NotCompliantWarningException x)
         {
            warning = x;
         }
      }

      if (warning != null) throw warning;
   }

   private void checkSameClassModifiers(Class jmxri, Class mx4j) throws NotCompliantException
   {
      int jmxriModifiers = jmxri.getModifiers();
      int mx4jModifers = mx4j.getModifiers();
      if (jmxriModifiers != mx4jModifers)
      {
         int modifier = jmxriModifiers ^ mx4jModifers;
         if ((modifier & jmxriModifiers) != 0)
         {
            throw new NotCompliantException("JMX class " + jmxri.getName() + " in MX4J implementation is not declared " + Modifier.toString(modifier) + " as it should be");
         }
         if ((modifier & mx4jModifers) != 0)
         {
            throw new NotCompliantWarningException("JMX class " + jmxri.getName() + " in MX4J implementation is declared " + Modifier.toString(modifier) + ", it is not in JMXRI");
         }
      }
   }

   private void checkSameInheritance(Class jmxri, Class mx4j) throws NotCompliantException
   {
      // I have to walk the inheritance hierarchy

      Set jmxriInterfaces = new HashSet();
      Set mx4jInterfaces = new HashSet();
      for (Class jmxriParent = jmxri, mx4jParent = mx4j; jmxriParent != null; jmxriParent = jmxriParent.getSuperclass(), mx4jParent = mx4jParent.getSuperclass())
      {
         findInterfaces(jmxriParent, jmxriInterfaces);

         findInterfaces(mx4jParent, mx4jInterfaces);

         if (!jmxriParent.getName().equals(mx4jParent.getName()))
         {
            throw new NotCompliantException("JMX class " + jmxri.getName() + " in MX4J implementation does not have the same hierarchy as JMXRI: " + mx4jParent.getName() + ", should be " + jmxriParent.getName());
         }
      }

      if (!jmxriInterfaces.containsAll(mx4jInterfaces))
      {
         mx4jInterfaces.removeAll(jmxriInterfaces);
         checkInterfacesHaveMethods(jmxri, mx4jInterfaces);
      }
      if (!mx4jInterfaces.containsAll(jmxriInterfaces))
      {
         jmxriInterfaces.removeAll(mx4jInterfaces);
         throw new NotCompliantException("JMX class " + jmxri.getName() + " in MX4J implementation does not implement the required interfaces: " + jmxriInterfaces);
      }
   }

   private void findInterfaces(Class cls, Set interfaces)
   {
      Class[] intfs = cls.getInterfaces();
      for (int i = 0; i < intfs.length; ++i)
      {
         Class intf = intfs[i];
         boolean added = interfaces.add(intf.getName());
         if (added) findInterfaces(intf, interfaces);
      }
   }

   private void checkInterfacesHaveMethods(Class cls, Set interfaces) throws NotCompliantException
   {
      boolean warning = false;
      for (Iterator i = interfaces.iterator(); i.hasNext();)
      {
         String name = (String)i.next();
         if (name.equals("java.lang.Cloneable"))
            warning = true;
         else
            warning = false;
      }

      if (warning)
         throw new NotCompliantWarningException("JMX class " + cls.getName() + " in MX4J implementation implements too many tag interfaces: " + interfaces);
      else
         throw new NotCompliantException("JMX class " + cls.getName() + " in MX4J implementation implements too many interfaces: " + interfaces);
   }

   private void checkSameConstructors(final Class jmxri, Class mx4j) throws NotCompliantException
   {
      checkSameObjectMethod(new ObjectClass.Constructor(jmxri), new ObjectClass.Constructor(mx4j));
   }

   private void checkSameMethods(Class jmxri, Class mx4j) throws NotCompliantException
   {
      checkSameObjectMethod(new ObjectClass.Method(jmxri), new ObjectClass.Method(mx4j));
   }

   private void checkSameObjectMethod(ObjectClass jmxri, ObjectClass mx4j) throws NotCompliantException
   {
      // Public methods first
      Set jmxriMethods = wrapMethods(jmxri.getMethods());
      Set mx4jMethods = wrapMethods(mx4j.getMethods());
      checkSameMethods(jmxri.getName(), jmxriMethods, mx4jMethods);

      // Protected methods now. I should walk the inheritance hierarchy.
      jmxriMethods.clear();
      mx4jMethods.clear();
      for (ObjectClass jmxriParent = jmxri, mx4jParent = mx4j; jmxriParent != null; jmxriParent = jmxriParent.getSuperclass(), mx4jParent = mx4jParent.getSuperclass())
      {
         ObjectMethod[] methods = jmxriParent.getDeclaredMethods();
         for (int i = 0; i < methods.length; ++i)
         {
            if (Modifier.isProtected(methods[i].getModifiers()))
            {
               jmxriMethods.add(wrapMethod(methods[i]));
            }
         }

         methods = mx4jParent.getDeclaredMethods();
         for (int i = 0; i < methods.length; ++i)
         {
            if (Modifier.isProtected(methods[i].getModifiers()))
            {
               mx4jMethods.add(wrapMethod(methods[i]));
            }
         }
      }
      checkSameMethods(jmxri.getName(), jmxriMethods, mx4jMethods);
   }

   private void checkSameFields(Class jmxri, Class mx4j) throws NotCompliantException
   {
      // Public fields first
      Set jmxriFields = wrapFields(jmxri.getFields());
      Set mx4jFields = wrapFields(mx4j.getFields());
      checkSameFields(jmxri.getName(), jmxriFields, mx4jFields);

      // Protected fields now. I should walk the inheritance hierarchy.
      jmxriFields.clear();
      mx4jFields.clear();
      for (Class jmxriParent = jmxri, mx4jParent = mx4j; jmxriParent != null; jmxriParent = jmxriParent.getSuperclass(), mx4jParent = mx4jParent.getSuperclass())
      {
         Field[] fields = jmxriParent.getDeclaredFields();
         for (int i = 0; i < fields.length; ++i)
         {
            if (Modifier.isProtected(fields[i].getModifiers()))
            {
               jmxriFields.add(wrapField(fields[i]));
            }
         }

         fields = mx4jParent.getDeclaredFields();
         for (int i = 0; i < fields.length; ++i)
         {
            if (Modifier.isProtected(fields[i].getModifiers()))
            {
               mx4jFields.add(wrapField(fields[i]));
            }
         }
      }
      checkSameFields(jmxri.getName(), jmxriFields, mx4jFields);
   }

   private void checkSameSerialVersionUID(Class jmxriClass, Class mx4jClass) throws NotCompliantException
   {
      try
      {
         Field jmxriField = jmxriClass.getField("serialVersionUID");
         jmxriField.setAccessible(true);
         Field mx4jField = mx4jClass.getField("serialVersionUID");
         mx4jField.setAccessible(true);
         long jmxriValue = jmxriField.getLong(null);
         long mx4jValue = jmxriField.getLong(null);
         if (jmxriValue != mx4jValue) throw new NotCompliantException("JMX class " + jmxriClass.getName() + " in MX4J implementation does not have the same serialVersionUID: expecting " + jmxriValue + ", found " + mx4jValue);
      }
      catch (NoSuchFieldException ignored)
      {
         // If the class did not change between JMX 1.0 and JMX 1.1, then the serialVersionUID is not present
      }
      catch (NotCompliantException x)
      {
         throw x;
      }
      catch (Exception x)
      {
         x.printStackTrace();
         throw new NotCompliantException("Unknown problems in checking serialVersionUID: " + x);
      }
   }

   private Set wrapMethods(ObjectMethod[] methods)
   {
      Set set = new HashSet();
      for (int i = 0; i < methods.length; ++i)
      {
         set.add(wrapMethod(methods[i]));
      }
      return set;
   }

   private MethodWrapper wrapMethod(ObjectMethod method)
   {
      return new MethodWrapper(method);
   }

   private Set wrapFields(Field[] fields)
   {
      HashSet set = new HashSet();
      for (int i = 0; i < fields.length; ++i)
      {
         set.add(wrapField(fields[i]));
      }
      return set;
   }

   private FieldWrapper wrapField(Field field)
   {
      return new FieldWrapper(field);
   }

   private void checkSameMethods(String name, Set jmxri, Set mx4j) throws NotCompliantException
   {
      if (!jmxri.containsAll(mx4j))
      {
         checkDifferentMethods(name, mx4j, jmxri);
      }

      if (!mx4j.containsAll(jmxri))
      {
         checkDifferentMethods(name, jmxri, mx4j);
      }
   }

   private void checkDifferentMethods(String name, Set set1, Set set2) throws NotCompliantException
   {
      set1.removeAll(set2);

      boolean warning = false;
      boolean error = false;
      ArrayList warnings = new ArrayList();
      ArrayList errors = new ArrayList();
      for (Iterator i = set1.iterator(); i.hasNext();)
      {
         MethodWrapper method1 = (MethodWrapper)i.next();
         boolean found = false;
         for (Iterator j = set2.iterator(); j.hasNext();)
         {
            MethodWrapper method2 = (MethodWrapper)j.next();
            if (method1.isSameMethod(method2))
            {
               if (!method1.sameSignatureModifiers(method2))
               {
                  warning = true;
                  warnings.add(method1);
                  warnings.add(method2);
               }
               else
               {
                  if (method1.throwsClauseDifferForRuntimeExceptionsOnly(method2))
                  {
                     warning = true;
                     warnings.add(method1);
                     warnings.add(method2);
                  }
                  else
                  {
                     error = true;
                     errors.add(method1);
                     errors.add(method2);
                  }
               }
               found = true;
               break;
            }
         }
         if (!found) throw new NotCompliantException("JMX class " + name + " in MX4J implementation has different interface: " + set1);
      }

      if (error) throw new NotCompliantException("JMX class " + name + " in MX4J implementation has different signature: " + errors);
      if (warning) throw new NotCompliantWarningException("JMX class " + name + " in MX4J implementation has different signature: " + warnings);
      throw new IllegalStateException();
   }

   private void checkSameFields(String name, Set jmxri, Set mx4j) throws NotCompliantException
   {
      if (!jmxri.containsAll(mx4j))
      {
         mx4j.removeAll(jmxri);
         throw new NotCompliantException("JMX class " + name + " in MX4J implementation has too many fields: " + mx4j);
      }
      if (!mx4j.containsAll(jmxri))
      {
         jmxri.removeAll(mx4j);
         throw new NotCompliantException("JMX class " + name + " in MX4J implementation does not have the required fields: " + jmxri);
      }
   }
}
