/******************************************************************************
 * Copyright (c) 2006, 2010 VMware Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * and Apache License v2.0 which accompanies this distribution. 
 * The Eclipse Public License is available at 
 * http://www.eclipse.org/legal/epl-v10.html and the Apache License v2.0
 * is available at http://www.opensource.org/licenses/apache2.0.php.
 * You may elect to redistribute this code under either of these licenses. 
 * 
 * Contributors:
 *   VMware Inc.
 *****************************************************************************/

package org.eclipse.gemini.blueprint.util.internal;

import java.lang.reflect.Modifier;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.osgi.framework.Bundle;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;

/**
 * Class utility used internally. Contains mainly class inheritance mechanisms used when creating OSGi service proxies.
 * 
 * @author Costin Leau
 * 
 */
public abstract class ClassUtils {

	private static class ReadOnlySetFromMap<E> implements Set<E> {

		private final Set<E> keys;

		public ReadOnlySetFromMap(Map<E, ?> lookupMap) {
			keys = lookupMap.keySet();
		}

		public boolean add(Object o) {
			throw new UnsupportedOperationException();
		}

		public boolean addAll(Collection<? extends E> c) {
			throw new UnsupportedOperationException();
		}

		public void clear() {
			throw new UnsupportedOperationException();
		}

		public boolean contains(Object o) {
			return keys.contains(o);
		}

		public boolean containsAll(Collection<?> c) {
			return keys.containsAll(c);
		}

		public boolean isEmpty() {
			return keys.isEmpty();
		}

		public Iterator<E> iterator() {
			return keys.iterator();
		}

		public boolean remove(Object o) {
			throw new UnsupportedOperationException();
		}

		public boolean removeAll(Collection<?> c) {
			throw new UnsupportedOperationException();
		}

		public boolean retainAll(Collection<?> c) {
			throw new UnsupportedOperationException();
		}

		public int size() {
			return keys.size();
		}

		public Object[] toArray() {
			return keys.toArray();
		}

		public <T> T[] toArray(T[] array) {
			return keys.toArray(array);
		}

		public String toString() {
			return keys.toString();
		}

		public int hashCode() {
			return keys.hashCode();
		}

		public boolean equals(Object o) {
			return o == this || keys.equals(o);
		}
	}

	public enum ClassSet {
		/** Include only the interfaces inherited from superclasses or implemented by the current class */
		INTERFACES,
		/** Include only the class hierarchy (interfaces are excluded) */
		CLASS_HIERARCHY,
		/** Include all inherited classes (classes or interfaces) */
		ALL_CLASSES;
	}

	/**
	 * List of special class loaders, outside OSGi, that might be used by the user through boot delegation. read-only.
	 */
	public static final List<ClassLoader> knownNonOsgiLoaders;

	/**
	 * Set of special class loaders, outside OSGi, that might be used by the user through boot delegation. read-only.
	 */
	public static final Set<ClassLoader> knownNonOsgiLoadersSet;

	// add the known, non-OSGi class loaders
	// note that the order is important
	static {
		// start with the framework class loader
		// then get all its parents (normally the this should be fwk -> (*) -> app -> ext -> boot)
		// where (*) represents some optional loaders for cases where the framework is embedded

		final Map<ClassLoader, Boolean> lookupMap = new ConcurrentHashMap<ClassLoader, Boolean>(8);
		final List<ClassLoader> lookupList = Collections.synchronizedList(new ArrayList<ClassLoader>());

		final ClassLoader classLoader = getFwkClassLoader();

		if (System.getSecurityManager() != null) {
			AccessController.doPrivileged(new PrivilegedAction<Object>() {
				public Object run() {
					addNonOsgiClassLoader(classLoader, lookupList, lookupMap);
					// get the system class loader
					ClassLoader sysLoader = ClassLoader.getSystemClassLoader();
					addNonOsgiClassLoader(sysLoader, lookupList, lookupMap);
					return null;
				}
			});
		} else {
			addNonOsgiClassLoader(classLoader, lookupList, lookupMap);
			// get the system class loader
			ClassLoader sysLoader = ClassLoader.getSystemClassLoader();
			addNonOsgiClassLoader(sysLoader, lookupList, lookupMap);
		}

		// wrap the fields as read-only collections
		knownNonOsgiLoaders = Collections.unmodifiableList(lookupList);
		knownNonOsgiLoadersSet = new ReadOnlySetFromMap<ClassLoader>(lookupMap);

	}

	public static ClassLoader getFwkClassLoader() {
		if (System.getSecurityManager() != null) {
			return AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
				public ClassLoader run() {
					return Bundle.class.getClassLoader();
				}
			});
		} else {
			return Bundle.class.getClassLoader();
		}
	}

	/**
	 * Special static method used during the class initialization.
	 * 
	 * @param classLoader non OSGi class loader
	 */
	private static void addNonOsgiClassLoader(ClassLoader classLoader, List<ClassLoader> list,
			Map<ClassLoader, Boolean> map) {
		while (classLoader != null) {
			synchronized (list) {
				if (!map.containsKey(classLoader)) {
					list.add(classLoader);
					map.put(classLoader, Boolean.TRUE);
				}
			}
			classLoader = classLoader.getParent();
		}
	}

	/**
	 * Simple class loading abstraction working on both ClassLoader and Bundle classes.
	 * 
	 * @author Costin Leau
	 * 
	 */
	private static class ClassLoaderBridge {

		private final Bundle bundle;

		private final ClassLoader classLoader;

		public ClassLoaderBridge(Bundle bundle) {
			Assert.notNull(bundle);
			this.bundle = bundle;
			this.classLoader = null;
		}

		public ClassLoaderBridge(ClassLoader classLoader) {
			Assert.notNull(classLoader);
			this.classLoader = classLoader;
			this.bundle = null;
		}

		public Class<?> loadClass(String className) throws ClassNotFoundException {
			return (bundle == null ? classLoader.loadClass(className) : bundle.loadClass(className));
		}

		public boolean canSee(String className) {
			return (bundle == null ? org.springframework.util.ClassUtils.isPresent(className, classLoader) : isPresent(
					className, bundle));
		}
	}

	/**
	 * Returns an array of parent classes for the given class. The mode paramater indicates whether only interfaces
	 * should be included, classes or both.
	 * 
	 * This method is normally used for publishing services and determing the
	 * {@link org.osgi.framework.Constants#OBJECTCLASS} property.
	 * 
	 * <p/> Note: this method does class expansion returning parent as well as children classes.
	 * 
	 * </p>
	 * 
	 * @see #INCLUDE_ALL_CLASSES
	 * @see #INCLUDE_CLASS_HIERARCHY
	 * @see #INCLUDE_INTERFACES
	 * 
	 * @param clazz
	 * @param mode
	 * 
	 * @return array of classes extended or implemented by the given class
	 */
	public static Class<?>[] getClassHierarchy(Class<?> clazz, ClassSet inclusion) {
		Class<?>[] classes = null;

		if (clazz != null) {

			Set<Class<?>> composingClasses = new LinkedHashSet<Class<?>>();
			boolean includeClasses =
					(inclusion.equals(ClassSet.CLASS_HIERARCHY) || inclusion.equals(ClassSet.ALL_CLASSES));
			boolean includeInterfaces =
					(inclusion.equals(ClassSet.INTERFACES) || inclusion.equals(ClassSet.ALL_CLASSES));

			Class<?> clz = clazz;
			do {
				if (includeClasses) {
					composingClasses.add(clz);
				}

				if (includeInterfaces) {
					CollectionUtils.mergeArrayIntoCollection(getAllInterfaces(clz), composingClasses);
				}

				clz = clz.getSuperclass();
			} while (clz != null && clz != Object.class);

			classes = (Class[]) composingClasses.toArray(new Class[composingClasses.size()]);
		}
		return (classes == null ? new Class[0] : classes);
	}

	/**
	 * Sugar method that determines the class hierarchy of a given class and then filtering based on the given
	 * classloader. If a null classloader, the one of the given class will be used.
	 * 
	 * @param clazz
	 * @param mode
	 * @param loader
	 * @return
	 */
	public static Class<?>[] getVisibleClassHierarchy(Class<?> clazz, ClassSet inclusion, ClassLoader loader) {
		if (clazz == null)
			return new Class[0];

		return getVisibleClasses(getClassHierarchy(clazz, inclusion), getClassLoader(clazz));
	}

	/**
	 * 'Sugar' method that determines the class hierarchy of the given class, returning only the classes visible to the
	 * given bundle.
	 * 
	 * @param clazz the class for which the hierarchy has to be determined
	 * @param mode discovery mode
	 * @param bundle bundle used for class visibility
	 * @return array of visible classes part of the hierarchy
	 */
	public static Class<?>[] getVisibleClassHierarchy(Class<?> clazz, ClassSet inclusion, Bundle bundle) {
		return getVisibleClasses(getClassHierarchy(clazz, inclusion), bundle);
	}

	/**
	 * Given an array of classes, eliminates the ones that cannot be loaded by the given classloader.
	 * 
	 * @return
	 */
	public static Class<?>[] getVisibleClasses(Class<?>[] classes, ClassLoader classLoader) {
		return getVisibleClasses(classes, new ClassLoaderBridge(classLoader));
	}

	/**
	 * Given an array of classes, eliminates the ones that cannot be loaded by the given bundle.
	 * 
	 * @return
	 */
	public static Class<?>[] getVisibleClasses(Class<?>[] classes, Bundle bundle) {
		return getVisibleClasses(classes, new ClassLoaderBridge(bundle));
	}

	private static Class<?>[] getVisibleClasses(Class<?>[] classes, ClassLoaderBridge loader) {
		if (ObjectUtils.isEmpty(classes))
			return classes;

		Set<Class<?>> classSet = new LinkedHashSet<Class<?>>(classes.length);
		CollectionUtils.mergeArrayIntoCollection(classes, classSet);

		// filter class collection based on visibility
		for (Iterator<Class<?>> iter = classSet.iterator(); iter.hasNext();) {
			Class<?> clzz = iter.next();
			if (!loader.canSee(clzz.getName())) {
				iter.remove();
			}
		}
		return (Class[]) classSet.toArray(new Class[classSet.size()]);
	}

	/**
	 * Gets all interfaces implemented by the given class. This method returns both parent and children interfaces (i.e.
	 * Map and SortedMap).
	 * 
	 * @param clazz
	 * @return all interfaces implemented by the given class.
	 */
	public static Class<?>[] getAllInterfaces(Class<?> clazz) {
		Assert.notNull(clazz);
		return getAllInterfaces(clazz, new LinkedHashSet<Class<?>>(8));
	}

	/**
	 * Gets all the interfaces recursively.
	 * 
	 * @param clazz
	 * @param interfaces
	 * @return
	 */
	private static Class<?>[] getAllInterfaces(Class<?> clazz, Set<Class<?>> interfaces) {
		Class<?>[] intfs = clazz.getInterfaces();
		CollectionUtils.mergeArrayIntoCollection(intfs, interfaces);

		for (int i = 0; i < intfs.length; i++) {
			getAllInterfaces(intfs[i], interfaces);
		}

		return (Class[]) interfaces.toArray(new Class[interfaces.size()]);
	}

	/**
	 * Checks the present of a class inside a bundle. This method returns true if the given bundle can load the given
	 * class or false otherwise.
	 * 
	 * @param className
	 * @param bundle
	 * @return
	 */
	public static boolean isPresent(String className, Bundle bundle) {
		Assert.hasText(className);
		Assert.notNull(bundle);

		try {
			bundle.loadClass(className);
			return true;
		} catch (Exception cnfe) {
			return false;
		}
	}

	/**
	 * Returns the classloader for the given class. This method deals with JDK classes which return by default, a null
	 * classloader.
	 * 
	 * @param clazz
	 * @return
	 */
	public static ClassLoader getClassLoader(Class<?> clazz) {
		Assert.notNull(clazz);
		ClassLoader loader = clazz.getClassLoader();
		return (loader == null ? ClassLoader.getSystemClassLoader() : loader);
	}

	/**
	 * Returns an array of class string names for the given classes.
	 * 
	 * @param array
	 * @return
	 */
	public static String[] toStringArray(Class<?>[] array) {
		if (ObjectUtils.isEmpty(array))
			return new String[0];

		String[] strings = new String[array.length];

		for (int i = 0; i < array.length; i++) {
			strings[i] = array[i].getName();
		}

		return strings;
	}

	/**
	 * Determines if multiple classes(not interfaces) are specified, without any relation to each other. Interfaces will
	 * simply be ignored.
	 * 
	 * @param classes an array of classes
	 * @return true if at least two classes unrelated to each other are found, false otherwise
	 */
	public static boolean containsUnrelatedClasses(Class<?>[] classes) {
		if (ObjectUtils.isEmpty(classes))
			return false;

		Class<?> clazz = null;
		// check if is more then one class specified
		for (int i = 0; i < classes.length; i++) {
			if (!classes[i].isInterface()) {
				if (clazz == null)
					clazz = classes[i];
				// check relationship
				else {
					if (clazz.isAssignableFrom(classes[i]))
						// clazz is a parent, switch with the child
						clazz = classes[i];
					else if (!classes[i].isAssignableFrom(clazz))
						return true;

				}
			}
		}

		// everything is in order
		return false;
	}

	/**
	 * Parses the given class array and eliminate parents of existing classes. Useful when creating proxies to minimize
	 * the number of implemented interfaces and redundant class information.
	 * 
	 * @see #containsUnrelatedClasses(Class[])
	 * @see #configureFactoryForClass(ProxyFactory, Class[])
	 * 
	 * @param classes array of classes
	 * @return a new array without superclasses
	 */
	public static Class<?>[] removeParents(Class<?>[] classes) {
		if (ObjectUtils.isEmpty(classes))
			return new Class[0];

		List<Class<?>> clazz = new ArrayList<Class<?>>(classes.length);
		for (int i = 0; i < classes.length; i++) {
			clazz.add(classes[i]);
		}

		// remove null elements
		while (clazz.remove(null)) {
		}

		// only one class is allowed
		// there can be multiple interfaces
		// parents of classes inside the array are removed

		boolean dirty;
		do {
			dirty = false;
			for (int i = 0; i < clazz.size(); i++) {
				Class<?> currentClass = clazz.get(i);
				for (int j = 0; j < clazz.size(); j++) {
					if (i != j) {
						if (currentClass.isAssignableFrom(clazz.get(j))) {
							clazz.remove(i);
							i--;
							dirty = true;
							break;
						}
					}
				}
			}
		} while (dirty);

		return (Class[]) clazz.toArray(new Class[clazz.size()]);
	}

	/**
	 * Based on the given class, properly instructs the ProxyFactory proxies. For additional sanity checks on the passed
	 * classes, check the methods below.
	 * 
	 * @see #containsUnrelatedClasses(Class[])
	 * @see #removeParents(Class[])
	 * 
	 * @param factory
	 * @param classes
	 */
	public static void configureFactoryForClass(ProxyFactory factory, Class<?>[] classes) {
		if (ObjectUtils.isEmpty(classes))
			return;

		for (int i = 0; i < classes.length; i++) {
			Class<?> clazz = classes[i];

			if (clazz.isInterface()) {
				factory.addInterface(clazz);
			} else {
				factory.setTargetClass(clazz);
				factory.setProxyTargetClass(true);
			}
		}
	}

	/**
	 * Loads classes with the given name, using the given classloader. {@link ClassNotFoundException} exceptions are
	 * being ignored. The return class array will not contain duplicates.
	 * 
	 * @param classNames array of classnames to load (in FQN format)
	 * @param classLoader classloader used for loading the classes
	 * @return an array of classes (can be smaller then the array of class names) w/o duplicates
	 */
	public static Class<?>[] loadClassesIfPossible(String[] classNames, ClassLoader classLoader) {
		if (ObjectUtils.isEmpty(classNames))
			return new Class[0];

		Assert.notNull(classLoader, "classLoader is required");
		Set<Class<?>> classes = new LinkedHashSet<Class<?>>(classNames.length);

		for (int i = 0; i < classNames.length; i++) {
			try {
				classes.add(classLoader.loadClass(classNames[i]));
			} catch (ClassNotFoundException ex) {
				// ignore
			}
		}

		return (Class[]) classes.toArray(new Class[classes.size()]);
	}

	/**
	 * Loads and returns the classes given as names, using the given class loader. Throws IllegalArgument exception if
	 * the classes cannot be loaded.
	 * 
	 * @param classNames array of class names
	 * @param classLoader class loader for loading the classes
	 * @return the loaded classes
	 */
	public static Class<?>[] loadClasses(String[] classNames, ClassLoader classLoader) {
		if (ObjectUtils.isEmpty(classNames))
			return new Class[0];

		Assert.notNull(classLoader, "classLoader is required");
		Set<Class<?>> classes = new LinkedHashSet<Class<?>>(classNames.length);

		for (int i = 0; i < classNames.length; i++) {
			classes.add(org.springframework.util.ClassUtils.resolveClassName(classNames[i], classLoader));
		}

		return (Class[]) classes.toArray(new Class[classes.size()]);
	}

	/**
	 * Excludes classes from the given array, which match the given modifier.
	 * 
	 * @see Modifier
	 * 
	 * @param classes array of classes (can be null)
	 * @param modifier class modifier
	 * @return array of classes (w/o duplicates) which does not have the given modifier
	 */
	public static Class<?>[] excludeClassesWithModifier(Class<?>[] classes, int modifier) {
		if (ObjectUtils.isEmpty(classes))
			return new Class[0];

		Set<Class<?>> clazzes = new LinkedHashSet<Class<?>>(classes.length);

		for (int i = 0; i < classes.length; i++) {
			if ((modifier & classes[i].getModifiers()) == 0)
				clazzes.add(classes[i]);
		}
		return (Class[]) clazzes.toArray(new Class[clazzes.size()]);
	}

	/**
	 * Returns the first matching class from the given array, that doens't belong to common libraries such as the JDK or
	 * OSGi API. Useful for filtering OSGi services by type to prevent class cast problems.
	 * 
	 * <p/> No sanity checks are done on the given array class.
	 * 
	 * @param classes array of classes
	 * @return a 'particular' (non JDK/OSGi) class if one is found. Else the first available entry is returned.
	 */
	public static Class<?> getParticularClass(Class<?>[] classes) {
		boolean hasSecurity = (System.getSecurityManager() != null);
		for (int i = 0; i < classes.length; i++) {
			final Class<?> clazz = classes[i];
			ClassLoader loader = null;
			if (hasSecurity) {
				loader = AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
					public ClassLoader run() {
						return clazz.getClassLoader();
					}
				});
			} else {
				loader = clazz.getClassLoader();
			}
			// quick boot/system check
			if (loader != null) {
				// consider known loaders
				if (!knownNonOsgiLoadersSet.contains(loader)) {
					return clazz;
				}
			}
		}

		return (ObjectUtils.isEmpty(classes) ? null : classes[0]);
	}
}