/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.aries.unittest.mocks;

import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * <p>This class represents a method call that has been or is expected to be 
 *   made. It encapsulates the class that the call was made on, the method
 *   that was invoked and the arguments passed.</p>
 */
public final class MethodCall
{
  /** An empty object array */
  private static Object[] EMPTY_OBJECT_ARRAY = new Object[0];
  /** The name of the class invoked */
  private String _className;
  /** The array of interfaces implemented by the class */
  private Class<?>[] _interfaces = new Class[0];
  /** The method invoked */
  private String _methodName;
  /** The arguments passed */
  private Object[] _arguments = EMPTY_OBJECT_ARRAY;
  /** The object invoked */
  private Object _invokedObject;
  /** A list of comparators to use, instead of the objects .equals methods */
  private static Map<Class<?>, Comparator<?>> equalsHelpers = new HashMap<Class<?>, Comparator<?>>();

  /* ------------------------------------------------------------------------ */
  /* MethodCall method                                    
  /* ------------------------------------------------------------------------ */
  /**
   * This constructor allows a MethodCall to be created when the class can be
   * located statically, rather than dynamically.
   * 
   * @param clazz      The class.
   * @param methodName The method name.
   * @param arguments  The arguments.
   */
  public MethodCall(Class<?> clazz, String methodName, Object ... arguments)
  {
    _className = clazz.getName();
    _methodName = methodName;
    _arguments = arguments;
  }

  /* ------------------------------------------------------------------------ */
  /* MethodCall method                                    
  /* ------------------------------------------------------------------------ */
  /**
   * This method is used by the Skeleton in order create an instance of a 
   * MethodCall representing an invoked interface.
   * 
   * NOTE: If possible changing this so the constructor does not need to be
   * default visibility would be good, given the problems with default 
   * visibility. 
   * 
   * @param invokedObject The object that was invoked.
   * @param methodName    The name of the method invoked.
   * @param arguments     The arguments passed.
   */
  MethodCall(Object invokedObject, String methodName, Object ... arguments)
  {
    _className = invokedObject.getClass().getName();
    _interfaces = invokedObject.getClass().getInterfaces();
    _methodName = methodName;

    this._arguments = (arguments == null) ? EMPTY_OBJECT_ARRAY : arguments;
    
    _invokedObject = invokedObject;
  }

  /* ------------------------------------------------------------------------ */
  /* getArguments method                                    
  /* ------------------------------------------------------------------------ */
  /**
   * This method returns the arguments.
   * 
   * @return The arguments.
   */
  public Object[] getArguments()
  {
    return _arguments;
  }

  /* ------------------------------------------------------------------------ */
  /* getClassName method                                    
  /* ------------------------------------------------------------------------ */
  /**
   * Returns the name of the class the method was invoked or was defined on.
   * 
   * @return the classname.
   */
  public String getClassName()
  {
    return _className;
  }

  /* ------------------------------------------------------------------------ */
  /* getMethodName method                                    
  /* ------------------------------------------------------------------------ */
  /**
   * Returns the name of the method that was (or will be) invoked.
   * 
   * @return the method name
   */
  public String getMethodName()
  {
    return _methodName;
  }

  /* ------------------------------------------------------------------------ */
  /* checkClassName method                                    
  /* ------------------------------------------------------------------------ */
  /**
   * This method checks that the class names specified in the method call are
   * compatible, i.e. one is a superclass of the other.
   * 
   * @param one The first method call.
   * @param two The second method call.
   * @return    true if the classes can be assigned to each other.
   */
  private boolean checkClassName(MethodCall one, MethodCall two)
  {
    // TODO make this stuff work better.
    if (one._className.equals("java.lang.Object"))
    {
      return true;
    }
    else if (two._className.equals("java.lang.Object"))
    {
      return true;
    }
    else if (one._className.equals(two._className))
    {
      return true;
    }
    else
    {
      // check the other class name is one of the implemented interfaces
      boolean result = false;
        
      for (int i = 0; i < two._interfaces.length; i++)
      {
        if (two._interfaces[i].getName().equals(one._className))
        {
          result = true;
          break;
        }
      }
        
      if (!result)
      {
        for (int i = 0; i < one._interfaces.length; i++)
        {
          if (one._interfaces[i].getName().equals(two._className))
          {
            result = true;
            break;
          }
        }
      }
        
      return result;
    }
  }
  
  /* ------------------------------------------------------------------------ */
  /* equals method                                    
  /* ------------------------------------------------------------------------ */
  /**
   * Returns true if and only if the two object represent the same call.
   * 
   * @param obj The object to be compared.
   * @return    true if the specified object is the same as this.
   */
  @Override
  public boolean equals(Object obj)
  {

    if (obj == null) return false;

    if (obj == this) return true;
    
    if (obj instanceof MethodCall)
    {
      MethodCall other = (MethodCall)obj;

      if (!checkClassName(this, other)) 
      {
        return false;
      }
      
      if (!other._methodName.equals(this._methodName)) return false;
      if (other._arguments.length != this._arguments.length) return false;
      
      for (int i = 0; i < this._arguments.length; i++)
      {
        boolean thisArgNull = this._arguments[i] == null;
        boolean otherArgClazz = other._arguments[i] instanceof Class;
        boolean otherArgNull = other._arguments[i] == null;
        boolean thisArgClazz = this._arguments[i] instanceof Class;
        
        if (thisArgNull)
        {
          if (otherArgNull)
          {
            // This is OK
          }
          else if (otherArgClazz)
          {
            // This is also OK
          }
          else
          {
            return false;
          }
          // this argument is OK.
        }
        else if (otherArgNull)
        {
          if (thisArgClazz)
          {
            // This is OK
          }
          else
          {
            return false;
          }
          // this argument is OK.
        }
        else if (otherArgClazz)
        {
          if (thisArgClazz)
          {
            Class<?> otherArgClass = (Class<?>) other._arguments[i];
            Class<?> thisArgClass = (Class<?>) this._arguments[i];
          
            if (otherArgClass.equals(Class.class) || thisArgClass.equals(Class.class))
            {
              // do nothing
            } else if (!(otherArgClass.isAssignableFrom(thisArgClass) ||
                thisArgClass.isAssignableFrom(otherArgClass)))
            {
              return false;
            }
          }
          else
          {
            Class<?> clazz = (Class<?>)other._arguments[i];
            if (clazz.isPrimitive())
            {
              if (clazz.equals(byte.class))
              {
                return this._arguments[i].getClass().equals(Byte.class);
              }
              else if (clazz.equals(boolean.class))
              {
                return this._arguments[i].getClass().equals(Boolean.class);
              }
              else if (clazz.equals(short.class))
              {
                return this._arguments[i].getClass().equals(Short.class);
              }
              else if (clazz.equals(char.class))
              {
                return this._arguments[i].getClass().equals(Character.class);
              }
              else if (clazz.equals(int.class))
              {
                return this._arguments[i].getClass().equals(Integer.class);
              }
              else if (clazz.equals(long.class))
              {
                return this._arguments[i].getClass().equals(Long.class);
              }
              else if (clazz.equals(float.class))
              {
                return this._arguments[i].getClass().equals(Float.class);
              }
              else if (clazz.equals(double.class))
              {
                return this._arguments[i].getClass().equals(Double.class);
              }
            }
            else
            {
              if (!clazz.isInstance(this._arguments[i]))
              {
                return false;
              }
            }
          }
        }
        else if (thisArgClazz)
        {
          Class<?> clazz = (Class<?>)this._arguments[i];
          if (clazz.isPrimitive())
          {
            if (clazz.equals(byte.class))
            {
              return other._arguments[i].getClass().equals(Byte.class);
            }
            else if (clazz.equals(boolean.class))
            {
              return other._arguments[i].getClass().equals(Boolean.class);
            }
            else if (clazz.equals(short.class))
            {
              return other._arguments[i].getClass().equals(Short.class);
            }
            else if (clazz.equals(char.class))
            {
              return other._arguments[i].getClass().equals(Character.class);
            }
            else if (clazz.equals(int.class))
            {
              return other._arguments[i].getClass().equals(Integer.class);
            }
            else if (clazz.equals(long.class))
            {
              return other._arguments[i].getClass().equals(Long.class);
            }
            else if (clazz.equals(float.class))
            {
              return other._arguments[i].getClass().equals(Float.class);
            }
            else if (clazz.equals(double.class))
            {
              return other._arguments[i].getClass().equals(Double.class);
            }
          }
          else
          {
            if (!clazz.isInstance(other._arguments[i]))
            {
              return false;
            }
          }
        }
        else if (this._arguments[i] instanceof Object[] && other._arguments[i] instanceof Object[])
        {
          return equals((Object[])this._arguments[i], (Object[])other._arguments[i]);
        }
        else 
        {
          int result = compareUsingComparators(this._arguments[i], other._arguments[i]);
          
          if (result == 0) continue;
          else if (result == 1) return false;
          else if (!this._arguments[i].equals(other._arguments[i])) return false;
        }
      }
    }

    return true;
  }

  /**
   * Compare two arrays calling out to the custom comparators and handling
   * AtomicIntegers nicely.
   * 
   * TODO remove the special casing for AtomicInteger.
   * 
   * @param arr1 
   * @param arr2
   * @return true if the arrays are equals, false otherwise.
   */
  private boolean equals(Object[] arr1, Object[] arr2)
  {
    if (arr1.length != arr2.length) return false;
    
    for (int k = 0; k < arr1.length; k++) {
      if (arr1[k] == arr2[k]) continue;
      if (arr1[k] == null && arr2[k] != null) return false;
      if (arr1[k] != null && arr2[k] == null) return false;
      
      int result = compareUsingComparators(arr1[k], arr2[k]);
      
      if (result == 0) continue;
      else if (result == 1) return false;
      
      if (arr1[k] instanceof AtomicInteger && arr2[k] instanceof AtomicInteger && 
          ((AtomicInteger)arr1[k]).intValue() == ((AtomicInteger)arr2[k]).intValue()) 
        continue;

      if (!arr1[k].equals(arr2[k])) return false;

    }
    
    return true;
  }
  
  /**
   * Attempt to do the comparison using the comparators. This logic returns:
   * 
   * <ul>
   *   <li>0 if they are equal</li>
   *   <li>1 if they are not equal</li>
   *   <li>-1 no comparison was run</li>
   * </ul>
   * 
   * @param o1 The first object.
   * @param o2 The second object.
   * @return 0, 1 or -1 depending on whether the objects were equal, not equal or no comparason was run.
   */
  private int compareUsingComparators(Object o1, Object o2)
  {
    if (o1.getClass() == o2.getClass()) {
      @SuppressWarnings("unchecked")
      Comparator<Object> compare = (Comparator<Object>) equalsHelpers.get(o1.getClass());
      
      if (compare != null) {
        if (compare.compare(o1, o2) == 0) return 0;
        else return 1;
      }
    }
    
    return -1;
  }
  
  /* ------------------------------------------------------------------------ */
  /* hashCode method                                    
  /* ------------------------------------------------------------------------ */
  /**
   * Returns the hashCode (obtained by returning the hashCode of the 
   * methodName).
   * 
   * @return The hashCode
   */
  @Override
  public int hashCode()
  {
    return _methodName.hashCode();
  }

  /* ------------------------------------------------------------------------ */
  /* toString method                                    
  /* ------------------------------------------------------------------------ */
  /**
   * Returns a string representation of the method call.
   * 
   * @return string representation.
   */
  @Override
  public String toString()
  {
    StringBuffer buffer = new StringBuffer();
    buffer.append(this._className);
    buffer.append('.');
    buffer.append(this._methodName);
    buffer.append("(");

    for (int i = 0; i < this._arguments.length; i++)
    {
      if (this._arguments[i] != null)
      {
        if (this._arguments[i] instanceof Class)
        {
          buffer.append(((Class<?>)this._arguments[i]).getName());
        }
        else if (Proxy.isProxyClass(this._arguments[i].getClass()))
        {
          // If the object is a dynamic proxy, just use the proxy class name to avoid calling toString on the proxy
          buffer.append(this._arguments[i].getClass().getName());
        }
        else if (this._arguments[i] instanceof Object[])
        {
          buffer.append(Arrays.toString((Object[])this._arguments[i]));
        }
        else
        {  
          buffer.append(String.valueOf(this._arguments[i]));
        }
      } 
      else
      {  
        buffer.append("null");
      }

      if (i + 1 < this._arguments.length)
        buffer.append(", ");
    }

    buffer.append(")");
    String string = buffer.toString();
    return string;
  }

  /* ------------------------------------------------------------------------ */
  /* getInterfaces method                                    
  /* ------------------------------------------------------------------------ */
  /**
   * This method returns the list of interfaces implemented by the class that
   * was called.
   * 
   * @return Returns the interfaces.
   */
  public Class<?>[] getInterfaces()
  {
    return this._interfaces;
  }
  
  /* ------------------------------------------------------------------------ */
  /* getInvokedObject method                                    
  /* ------------------------------------------------------------------------ */
  /**
   * This method returns the invoked object.
   * 
   * @return The object that was invoked or null if an expected call.
   */
  public Object getInvokedObject()
  {
    return _invokedObject;
  }
  
  /* ------------------------------------------------------------------------ */
  /* registerEqualsHelper method                                    
  /* ------------------------------------------------------------------------ */
  /**
   * The native equals for an object may not provide the behaviour required by
   * the tests. As an example AtomicInteger does not define a .equals, but tests
   * may wish to compare it being passed in a method call for equality. This
   * method allows a Comparator to be specified for any type and the Comparator
   * will be used to determine equality in place of the .equals method.
   * 
   * <p>The Comparator must not throw exceptions, and must return 0 for equality
   *   or any other integer for inequality.
   * </p>
   * 
   * @param <T>        the type of the class and comparator.
   * @param type       the type of the class for which the comparator will be called.
   * @param comparator the comparator to call.
   */
  public static <T> void registerEqualsHelper(Class<T> type, Comparator<T> comparator)
  {
    equalsHelpers.put(type, comparator);
  }
  
  /* ------------------------------------------------------------------------ */
  /* removeEqualsHelper method                                    
  /* ------------------------------------------------------------------------ */
  /**
   * This method removes any registered comparator specified for the given type.
   * 
   * @param type the type to remove the comparator from.
   */
  public static void removeEqualsHelper(Class<?> type)
  {
    equalsHelpers.remove(type);
  }
}