/*
 * This program is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
 * Foundation.
 *
 * You should have received a copy of the GNU Lesser General Public License along with this
 * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
 * or from the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * This program 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.
 *
 * Copyright (c) 2007 - 2009 Pentaho Corporation and Contributors.  All rights reserved.
 */

package org.pentaho.reporting.libraries.base.util;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Array;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.StringTokenizer;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * A collection of useful static utility methods for handling classes and object instantiation.
 *
 * @author Thomas Morgner
 */
public final class ObjectUtilities
{
  private static final Log LOGGER = LogFactory.getLog(ObjectUtilities.class);

  /**
   * A constant for using the TheadContext as source for the classloader.
   */
  public static final String THREAD_CONTEXT = "ThreadContext";
  /**
   * A constant for using the ClassContext as source for the classloader.
   */
  public static final String CLASS_CONTEXT = "ClassContext";

  /**
   * By default use the thread context.
   */
  private static String classLoaderSource = THREAD_CONTEXT;
  /**
   * The custom classloader to be used (if not null).
   */
  private static ClassLoader classLoader;
  private static final Integer[] EMPTY_VERSIONS = new Integer[0];

  /**
   * Default constructor - private.
   */
  private ObjectUtilities()
  {
  }

  /**
   * Returns the internal configuration entry, whether the classloader of the thread context or the context classloader
   * should be used.
   *
   * @return the classloader source, either THREAD_CONTEXT or CLASS_CONTEXT.
   */
  public static String getClassLoaderSource()
  {
    return classLoaderSource;
  }

  /**
   * Defines the internal configuration entry, whether the classloader of the thread context or the context classloader
   * should be used.
   * <p/>
   * This setting can only be defined using the API, there is no safe way to put this into an external configuration
   * file.
   *
   * @param classLoaderSource the classloader source, either THREAD_CONTEXT or CLASS_CONTEXT.
   */
  public static void setClassLoaderSource(final String classLoaderSource)
  {
    ObjectUtilities.classLoaderSource = classLoaderSource;
  }

  /**
   * Returns <code>true</code> if the two objects are equal OR both <code>null</code>.
   *
   * @param o1 object 1 (<code>null</code> permitted).
   * @param o2 object 2 (<code>null</code> permitted).
   * @return <code>true</code> or <code>false</code>.
   */
  public static boolean equal(final Object o1, final Object o2)
  {
    if (o1 == o2)
    {
      return true;
    }
    if (o1 != null)
    {
      return o1.equals(o2);
    }
    else
    {
      return false;
    }
  }
  
  /**
   * Performs a comparison on two file objects to determine if they refer to the 
   * same file. The <code>File.equals()</code> method requires that the files refer
   * to the same file in the same way (relative vs. absolute).
   * 
   * @param file1 the first file (<code>null</code> permitted).
   * @param file2 the second file (<code>null</code> permitted).
   * @return <code>true</code> if the files refer to the same file, <code>false</code> otherwise
   */
  public static boolean equals(final File file1, final File file2)
  {
    if (file1 == file2)
    {
      return true;
    }
    if (file1 != null && file2 != null)
    {
      try
      {
        return file1.getCanonicalFile().equals(file2.getCanonicalFile());
      }
      catch(IOException ioe)
      {
        // There was an error accessing the filesystem
        return file1.equals(file2);
      }
    }
    return false;
  }

  /**
   * Returns a clone of the specified object, if it can be cloned, otherwise throws a CloneNotSupportedException.
   *
   * @param object the object to clone (<code>null</code> not permitted).
   * @return A clone of the specified object.
   * @throws CloneNotSupportedException if the object cannot be cloned.
   */
  public static Object clone(final Object object)
      throws CloneNotSupportedException
  {
    if (object == null)
    {
      throw new IllegalArgumentException("Null 'object' argument.");
    }
    final Class aClass = object.getClass();
    if (aClass.isArray())
    {
      final int length = Array.getLength(object);
      final Object clone = Array.newInstance(aClass.getComponentType(), length);
      //noinspection SuspiciousSystemArraycopy
      System.arraycopy(object, 0, clone, 0, length);
      return object;
    }

    try
    {
      final Method method = aClass.getMethod("clone", (Class[]) null);
      if (Modifier.isPublic(method.getModifiers()))
      {
        return method.invoke(object, (Object[]) null);
      }
      throw new CloneNotSupportedException("Failed to clone: Method 'clone()' is not public on class " + aClass);
    }
    catch (NoSuchMethodException e)
    {
      LOGGER.warn("Object without clone() method is impossible on class " + aClass, e);
    }
    catch (IllegalAccessException e)
    {
      LOGGER.warn("Object.clone(): unable to call method 'clone()'  on class " + aClass, e);
    }
    catch (InvocationTargetException e)
    {
      LOGGER.warn("Object without clone() method is impossible on class " + aClass, e);
    }
    throw new CloneNotSupportedException
        ("Failed to clone: Clone caused an Exception while cloning type " + aClass);

  }

  /**
   * Redefines the custom classloader.
   *
   * @param classLoader the new classloader or null to use the default.
   */
  public static synchronized void setClassLoader(final ClassLoader classLoader)
  {
    ObjectUtilities.classLoader = classLoader;
  }

  /**
   * Returns the custom classloader or null, if no custom classloader is defined.
   *
   * @return the custom classloader or null to use the default.
   */
  public static ClassLoader getClassLoader()
  {
    return classLoader;
  }

  /**
   * Returns the classloader, which was responsible for loading the given class.
   *
   * @param c the classloader, either an application class loader or the boot loader.
   * @return the classloader, never null.
   * @throws SecurityException if the SecurityManager does not allow to grab the context classloader.
   */
  public static ClassLoader getClassLoader(final Class c)
  {
    final String localClassLoaderSource;
    synchronized (ObjectUtilities.class)
    {
      if (classLoader != null)
      {
        return classLoader;
      }
      localClassLoaderSource = classLoaderSource;
    }

    if ("ThreadContext".equals(localClassLoaderSource))
    {
      final ClassLoader threadLoader = Thread.currentThread().getContextClassLoader();
      if (threadLoader != null)
      {
        return threadLoader;
      }
    }

    // Context classloader - do not cache ..
    final ClassLoader applicationCL = c.getClassLoader();
    if (applicationCL == null)
    {
      return ClassLoader.getSystemClassLoader();
    }
    else
    {
      return applicationCL;
    }
  }


  /**
   * Returns the resource specified by the <strong>absolute</strong> name.
   *
   * @param name the name of the resource
   * @param c    the source class
   * @return the url of the resource or null, if not found.
   */
  public static URL getResource(final String name, final Class c)
  {
    if (name == null)
    {
      throw new NullPointerException();
    }

    final ClassLoader cl = getClassLoader(c);
    if (cl == null)
    {
      return null;
    }
    return cl.getResource(name);
  }

  /**
   * Returns the resource specified by the <strong>relative</strong> name.
   *
   * @param name the name of the resource relative to the given class
   * @param c    the source class
   * @return the url of the resource or null, if not found.
   */
  public static URL getResourceRelative(final String name, final Class c)
  {
    if (name == null)
    {
      throw new NullPointerException();
    }

    final ClassLoader cl = getClassLoader(c);
    final String cname = convertName(name, c);
    if (cl == null)
    {
      return null;
    }
    return cl.getResource(cname);
  }

  /**
   * Transform the class-relative resource name into a global name by appending it to the classes package name. If the
   * name is already a global name (the name starts with a "/"), then the name is returned unchanged.
   *
   * @param name the resource name
   * @param c    the class which the resource is relative to
   * @return the tranformed name.
   */
  private static String convertName(final String name, Class c)
  {
    if (name.length() > 0 && name.charAt(0) == '/')
    {
      // strip leading slash..
      return name.substring(1);
    }

    // we cant work on arrays, so remove them ...
    while (c.isArray())
    {
      c = c.getComponentType();
    }
    // extract the package ...
    final String baseName = c.getName();
    final int index = baseName.lastIndexOf('.');
    if (index == -1)
    {
      return name;
    }

    final String pkgName = baseName.substring(0, index);
    return pkgName.replace('.', '/') + '/' + name;
  }

  /**
   * Returns the inputstream for the resource specified by the <strong>absolute</strong> name.
   *
   * @param name    the name of the resource
   * @param context the source class
   * @return the url of the resource or null, if not found.
   */
  public static InputStream getResourceAsStream(final String name,
                                                final Class context)
  {
    final URL url = getResource(name, context);
    if (url == null)
    {
      return null;
    }

    try
    {
      return url.openStream();
    }
    catch (IOException e)
    {
      return null;
    }
  }

  /**
   * Returns the inputstream for the resource specified by the <strong>relative</strong> name.
   *
   * @param name    the name of the resource relative to the given class
   * @param context the source class
   * @return the url of the resource or null, if not found.
   */
  public static InputStream getResourceRelativeAsStream
      (final String name, final Class context)
  {
    final URL url = getResourceRelative(name, context);
    if (url == null)
    {
      return null;
    }

    try
    {
      return url.openStream();
    }
    catch (IOException e)
    {
      return null;
    }
  }

  /**
   * Tries to create a new instance of the given class. This is a short cut for the common bean instantiation code.
   *
   * @param className the class name as String, never null.
   * @param source    the source class, from where to get the classloader.
   * @return the instantiated object or null, if an error occured.
   * @deprecated This class is not typesafe and instantiates the specified object without any additional checks.
   */
  public static Object loadAndInstantiate(final String className,
                                          final Class source)
  {
    return loadAndInstantiate(className, source, null);
  }

  /**
   * Tries to create a new instance of the given class. This is a short cut for the common bean instantiation code. This
   * method is a type-safe method and will not instantiate the class unless it is an instance of the given type.
   *
   * @param className the class name as String, never null.
   * @param source    the source class, from where to get the classloader.
   * @param type      the expected type of the object that is being instantiated.
   * @return the instantiated object, which is guaranteed to be of the given type, or null, if an error occured.
   */
  public static Object loadAndInstantiate(final String className,
                                          final Class source,
                                          final Class type)
  {
    if (className == null || className.length() == 0)
    {
      return null;
    }
    try
    {
      final ClassLoader loader = getClassLoader(source);
      final Class c = Class.forName(className, false, loader);
      if (type != null && type.isAssignableFrom(c) == false)
      {
        // this is unacceptable and means someone messed up the configuration
        LOGGER.warn("Specified class " + className + " is not of expected type " + type);
        return null;
      }
      return c.newInstance();
    }
    catch (ClassNotFoundException e)
    {
      if (LOGGER.isDebugEnabled())
      {
        LOGGER.debug("Specified class " + className + " does not exist.");
      }
      // sometimes, this one is expected.
    }
    catch (NoClassDefFoundError e)
    {
      if (LOGGER.isDebugEnabled())
      {
        LOGGER.debug("Specified class " + className + " cannot be loaded [NOCLASSDEFERROR].");
      }
    }
    catch (Throwable e)
    {
      // this is more severe than a class not being found at all
      if (LOGGER.isDebugEnabled())
      {
        LOGGER.info("Specified class " + className + " failed to instantiate correctly.", e);
      }
      else
      {
        LOGGER.info("Specified class " + className + " failed to instantiate correctly.");
      }
    }
    return null;
  }


  /**
   * Checks whether the current JDK is at least JDK 1.4.
   *
   * @return true, if the JDK has been recognized as JDK 1.4, false otherwise.
   * @noinspection AccessOfSystemProperties
   */
  public static boolean isJDK14()
  {
    try
    {
      final ClassLoader loader = getClassLoader(ObjectUtilities.class);
      if (loader != null)
      {
        try
        {
          Class.forName("java.util.RandomAccess", false, loader);
          return true;
        }
        catch (ClassNotFoundException e)
        {
          return false;
        }
      }
    }
    catch (Exception e)
    {
      // the safe test failed, now lets check the system properties ...
    }
    // OK, the quick and dirty, but secure way failed. Lets try it
    // using the standard way.
    try
    {
      final String version = System.getProperty
          ("java.vm.specification.version");
      // parse the beast...
      if (version == null)
      {
        return false;
      }

      final Integer[] versions = parseVersions(version);
      final Integer[] target = new Integer[]{new Integer(1), new Integer(4)};
      return (ObjectUtilities.compareVersionArrays(versions, target) >= 0);
    }
    catch (Exception e)
    {
      // if that fails too, we assume the safe "no-JDK 1.4" mode.
      return false;
    }
  }

  /**
   * Compares version numbers.
   *
   * @param a1 the first array.
   * @param a2 the second array.
   * @return -1 if a1 is less than a2, 0 if a1 and a2 are equal, and +1 otherwise.
   */
  public static int compareVersionArrays(final Integer[] a1, final Integer[] a2)
  {
    final int length = Math.min(a1.length, a2.length);
    for (int i = 0; i < length; i++)
    {
      final Integer o1 = a1[i];
      final Integer o2 = a2[i];
      if (o1 == null && o2 == null)
      {
        // cannot decide ..
        continue;
      }
      if (o1 == null)
      {
        return 1;
      }
      if (o2 == null)
      {
        return -1;
      }
      final int retval = o1.compareTo(o2);
      if (retval != 0)
      {
        return retval;
      }
    }
    return 0;
  }

  /**
   * Parses a version string into numbers.
   *
   * @param version the version.
   * @return the parsed version array.
   */
  public static Integer[] parseVersions(final String version)
  {
    if (version == null)
    {
      return EMPTY_VERSIONS;
    }

    final ArrayList versions = new ArrayList();
    final StringTokenizer strtok = new StringTokenizer(version, ".");
    while (strtok.hasMoreTokens())
    {
      try
      {
        versions.add(new Integer(strtok.nextToken()));
      }
      catch (NumberFormatException nfe)
      {
        break;
      }
    }
    return (Integer[]) versions.toArray(new Integer[versions.size()]);
  }

  /**
   * Compares two arrays and determines if they are equal. This method allows to pass <code>null</code> null
   * references for any of the arrays.
   *
   * @param array1 the first array to compare.
   * @param array2 the second array to compare.
   * @return true, if both arrays are equal or both arrays are null, false otherwise.
   */
  public static boolean equalArray(final Object[] array1, final Object[] array2)
  {
    if (array1 == array2)
    {
      return true;
    }
    else if (array1 == null || array2 == null)
    {
      return false;
    }
    else
    {
      return Arrays.equals(array1, array2);
    }
  }

  public static int hashCode(final Object[] array1)
  {
    if (array1 == null)
    {
      return 0;
    }

    int hashCode = 31;
    for (int i = 0; i < array1.length; i++)
    {
      final Object o = array1[i];
      hashCode = hashCode * 31 + (o == null ? 0 : o.hashCode());
    }
    return hashCode;
  }
}