/*
 * 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.test.ws.tools.fixture;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import org.jboss.logging.Logger;

import com.thoughtworks.qdox.JavaDocBuilder;
import com.thoughtworks.qdox.model.AbstractJavaEntity;
import com.thoughtworks.qdox.model.JavaClass;
import com.thoughtworks.qdox.model.JavaField;
import com.thoughtworks.qdox.model.JavaMethod;
import com.thoughtworks.qdox.model.JavaSource;
import com.thoughtworks.qdox.model.Type;

/**
 *  Compares whether two Java source files are identical
 *  Uses QDox.  [Based on APITestCase of QDox Testsuite]
 *  @author <mailto:Anil.Saldhana@jboss.org>Anil Saldhana
 *  @since  Mar 7, 2005
 */
public class JBossSourceComparator
{
   // provide logging
   private static Logger log = Logger.getLogger(JBossSourceComparator.class);
   private JavaDocBuilder builder = null;
   private JavaSource source1 = null;
   private JavaSource source2 = null;

   /*
    * Needed to sort JavaClass, JavaField and JavaMethod as they
    * don't implement Comparable
    */

   private static Comparator ENTITY_COMPARATOR = new Comparator() {
      public int compare(Object obj1, Object obj2)
      {
         AbstractJavaEntity entity1 = (AbstractJavaEntity)obj1;
         AbstractJavaEntity entity2 = (AbstractJavaEntity)obj2;
         return entity1.getName().compareTo(entity2.getName());
      }
   };

   public JBossSourceComparator()
   {
      if (builder == null)
         builder = new JavaDocBuilder();
   }

   public JBossSourceComparator(File dir)
   {
      this();
      builder.addSourceTree(dir);

   }

   public JBossSourceComparator(String file1, String file2) throws Exception
   {
      this();
      try
      {
         source1 = builder.addSource(new FileReader(file1));
         source2 = builder.addSource(new FileReader(file2));
      }
      catch (FileNotFoundException e)
      {
         log.debug("Validation Failed:", e);
         throw e;
      }
   }

   public JBossSourceComparator(File file1, File file2) throws Exception
   {
      this();
      try
      {
         source1 = builder.addSource(file1);
         source2 = builder.addSource(file2);
      }
      catch (FileNotFoundException e)
      {
         log.debug("Validation Failed:", e);
         throw e;
      }
      catch (UnsupportedEncodingException e)
      {
         log.debug("Validation Failed:", e);
         throw e;
      }
   }

   /**
    * Needs to be called seperately if you are interested in
    * checking the imports.
    * Only checks on the exact imports.
    * java.util.* and java.util.Vector are not exact types.
    * @throws Exception
    */
   public void validateImports() throws Exception
   {
      String[] imp1 = source1.getImports();
      String[] imp2 = source2.getImports();

      if (imp1 == null && imp2 == null)
         return;
      if (imp1 == null && imp2 != null)
         throw new Exception("Imports are not equal");
      if (imp1 != null && imp2 == null)
         throw new Exception("Imports are not equal");
      //Compare the arrays
      int lenA = imp1.length;
      int lenB = imp2.length;
      if (lenA != lenB)
         throw new Exception("Imports are not equal");
      if (!Arrays.equals(imp1, imp2))
         throw new Exception("Imports are not equal");
   }

   /**
    * Validates the equality of two source files.
    * Does not validate imports (@see validateImports())
    * @return
    * @throws Exception
    */
   public boolean validate() throws Exception
   {
      boolean result = true;
      if (source1 == null)
         throw new IllegalArgumentException("First Java Source file missing");

      if (source2 == null)
         throw new IllegalArgumentException("Second Java Source file missing");

      try
      {
         validateAPI(source1, source2);
      }
      catch (Exception e)
      {
         log.error("Validation Failed:", e);
         result = false;
         throw e;
      }
      return result;
   }

   public boolean validateAPI(JavaSource expected, JavaSource actual) throws Exception
   {
      boolean result = false;
      List expectedClasses = Arrays.asList(expected.getClasses());
      Collections.sort(expectedClasses, ENTITY_COMPARATOR);
      List actualClasses = Arrays.asList(actual.getClasses());
      Collections.sort(actualClasses, ENTITY_COMPARATOR);

      //Check number of classes
      if (expectedClasses.size() != actualClasses.size())
         return (result = false);

      for (int i = 0; i < expectedClasses.size(); i++)
      {
         checkClassesEqual((JavaClass)expectedClasses.get(i), (JavaClass)actualClasses.get(i));
      }

      return result;
   }

   private void checkClassesEqual(JavaClass expected, JavaClass actual) throws Exception
   {
      if (!expected.getPackage().equals(actual.getPackage()))
         throw new Exception("Package names should be equal");
      checkModifiersEquals("Class modifiers should be equal", expected, actual);
      if (!expected.getName().equals(actual.getName()))
         throw new Exception("Class names should be equal");
      if ((expected.getSuperJavaClass() != null) && (actual.getSuperJavaClass() != null))
      {
         if (!expected.getSuperJavaClass().getName().equals(actual.getSuperJavaClass().getName()))
            throw new Exception("Super class should be equal");
      }
      if ((expected.getSuperJavaClass() == null) ^ (actual.getSuperJavaClass() == null))
      {
         throw new Exception("Super class should be equal");
      }
      checkInterfacesEqual(expected, actual);
      checkInnerClassesEquals(expected, actual);
      checkFieldsEqual(expected, actual);
      checkMethodsEqual(expected, actual);

   }

   private void checkFieldEquals(JavaField expected, JavaField actual) throws Exception
   {
      StringBuffer message = new StringBuffer("-> checkFieldEquals");
      message.append(" Expected : ");
      message.append(expected);
      message.append(" Actual : ");
      message.append(actual);

      if (!expected.getType().equals(actual.getType()))
         throw new Exception(message.toString() + "Field types should be equal");

      if (!expected.getName().equals(actual.getName()))
         throw new Exception(message.toString() + "Field names should be equal");

      checkModifiersEquals(message.toString() + "Field modifiers should be equal", expected, actual);
   }

   private void checkFieldsEqual(JavaClass expected, JavaClass actual) throws Exception
   {
      List expectedFields = Arrays.asList(expected.getFields());
      Collections.sort(expectedFields, ENTITY_COMPARATOR);
      List actualFields = Arrays.asList(actual.getFields());
      Collections.sort(actualFields, ENTITY_COMPARATOR);

      StringBuffer message = new StringBuffer("-> checkFieldsEqual");
      message.append(" Expected : ");
      message.append(expectedFields);
      message.append(" Actual : ");
      message.append(actualFields);

      if (expectedFields.size() != actualFields.size())
         throw new Exception(message.toString() + "Number of fields should be equal");

      for (int i = 0; i < expectedFields.size(); i++)
      {
         checkFieldEquals((JavaField)expectedFields.get(i), (JavaField)actualFields.get(i));
      }
   }

   private void checkInnerClassesEquals(JavaClass expected, JavaClass actual) throws Exception
   {
      List expectedInnerClasses = Arrays.asList(expected.getInnerClasses());
      Collections.sort(expectedInnerClasses, ENTITY_COMPARATOR);
      List actualInnerClasses = Arrays.asList(actual.getInnerClasses());
      Collections.sort(actualInnerClasses, ENTITY_COMPARATOR);

      StringBuffer message = new StringBuffer("-> checkInnerClassesEquals");
      message.append(" Expected : ");
      message.append(expectedInnerClasses);
      message.append(" Actual : ");
      message.append(actualInnerClasses);

      if (expectedInnerClasses.size() != actualInnerClasses.size())
         throw new Exception(message.toString() + "Number of inner classes should be equal");
      for (int i = 0; i < expectedInnerClasses.size(); i++)
      {
         checkClassesEqual((JavaClass)expectedInnerClasses.get(i), (JavaClass)actualInnerClasses.get(i));
      }

   }

   private void checkInterfacesEqual(JavaClass expected, JavaClass actual) throws Exception
   {
      List expectedImplements = Arrays.asList(expected.getImplements());
      Collections.sort(expectedImplements);
      List actualImplements = Arrays.asList(actual.getImplements());
      Collections.sort(actualImplements);
      StringBuffer message = new StringBuffer("-> checkInterfacesEqual");
      message.append(" Expected : ");
      message.append(expectedImplements);
      message.append(" Actual : ");
      message.append(actualImplements);

      if (expectedImplements.size() != actualImplements.size())
         throw new Exception(message.toString() + "Number of implemented interface should be equal");

      for (int i = 0; i < expectedImplements.size(); i++)
      {
         if (!expectedImplements.get(i).equals(actualImplements.get(i)))
            throw new Exception("Implemented interface should be equal");
      }
   }

   private void checkMethodsEqual(JavaClass expected, JavaClass actual) throws Exception
   {
      List expectedMethods = Arrays.asList(expected.getMethods());
      Collections.sort(expectedMethods, ENTITY_COMPARATOR);
      List actualMethods = Arrays.asList(actual.getMethods());
      Collections.sort(actualMethods, ENTITY_COMPARATOR);

      StringBuffer message = new StringBuffer("-> checkMethodsEqual");
      message.append(" Expected : ");
      message.append(expectedMethods);
      message.append(" Actual : ");
      message.append(actualMethods);

      if (expectedMethods.size() != actualMethods.size())
         throw new Exception(message.toString() + "Number of methods should be equal");
      for (int i = 0; i < expectedMethods.size(); i++)
      {
         JavaMethod expectedMethod = (JavaMethod)expectedMethods.get(i);
         JavaMethod actualMethod = (JavaMethod)actualMethods.get(i);

         if (expectedMethod.equals(actualMethod) == false)
         {
            throw new Exception("Method " + expectedMethod + "and " + actualMethod + " should be equal");
         }

         checkExceptionsEqual(expectedMethod, actualMethod);
      }
   }

   private void checkExceptionsEqual(JavaMethod expected, JavaMethod actual) throws Exception
   {
      List expectedExceptions = Arrays.asList(expected.getExceptions());
      Collections.sort(expectedExceptions);
      List actualExceptions = Arrays.asList(actual.getExceptions());
      Collections.sort(actualExceptions);

      StringBuffer message = new StringBuffer("-> checkExceptionsEqual");
      message.append(" Expected : ");
      message.append(expectedExceptions);
      message.append(" Actual : ");
      message.append(actualExceptions);

      if (expectedExceptions.size() != actualExceptions.size())
      {
         throw new Exception(message + " Number of exceptions should be equal");
      }

      for (int j = 0; j < expectedExceptions.size(); j++)
      {
         Type expectedException = (Type)expectedExceptions.get(j);
         Type actualException = (Type)actualExceptions.get(j);

         if (expectedException.equals(actualException) == false)
         {
            throw new Exception("Exception " + expectedException + " and " + actualException + " should be equal for method " + expected);
         }
      }
   }

   private void checkModifiersEquals(String msg, AbstractJavaEntity expected, AbstractJavaEntity actual) throws Exception
   {

      List expectedModifiers = Arrays.asList(expected.getModifiers());
      Collections.sort(expectedModifiers);
      List actualModifiers = Arrays.asList(actual.getModifiers());
      Collections.sort(actualModifiers);
      StringBuffer message = new StringBuffer("-> checkModifiersEquals");

      message.append(" Expected : ");
      message.append(expectedModifiers);
      message.append(" Actual : ");

      message.append(actualModifiers);
      if (expectedModifiers.size() != actualModifiers.size())
         throw new Exception(message.toString() + msg + " Number of modifiers should be equal ");

      for (int i = 0; i < expectedModifiers.size(); i++)
      {

         if (!expectedModifiers.get(i).equals(actualModifiers.get(i)))
            throw new Exception(msg + message.toString() + "Modifier should be equal");
      }
   }

}
