/*******************************************************************************
 *   Copyright 2014 Analog Devices, Inc.
 *
 *   Licensed 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 com.analog.lyric.collect;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.reflect.ClassPath;

import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.ThreadSafe;

/**
 * Simple registry mapping class names to constructor instances.
 * <p>
 * Note that {@link Map} methods such as {@link #containsKey}, {@link #containsValue},
 * {@link #keySet}, {@link #values}, {@link #entrySet}, and {@link #size} will only reflect keys
 * that have been explicitly looked up using one of {@link #get}, {@link #getClass},
 * {@link #instantiate} or {@link #loadAll()}.
 * <p>
 * 
 * @param <T> is the super class of all of the classes in the registry.
 * @since 0.07
 * @author Christopher Barber
 */
@ThreadSafe
public class ConstructorRegistry<T> extends AbstractMap<String, Constructor<T>>
{
	private final Class<? super T> _superClass;

	/**
	 * The number of packages specified in the constructor.
	 */
	private final int _nInitialPackages;
	
	@GuardedBy("this")
	protected final ArrayList<String> _packages;

	@GuardedBy("this")
	protected final ArrayListMultimap<String, Constructor<T>> _nameToConstructors;

	/*--------------
	 * Construction
	 */

	/**
	 * Constructs a new registry instance for given super class.
	 * <p>
	 * The registry will automatically search for classes in the same package in which
	 * {@code superClass} is declared. Additional packages may be added using {@link #addPackage}.
	 * <p>
	 * 
	 * @param superClass is the runtime class type corresponding to the declared parameter type
	 *        {@code T}.
	 * @since 0.07
	 */
	public ConstructorRegistry(Class<? super T> superClass)
	{
		this(superClass, new String[] { superClass.getPackage().getName() });
	}

	/**
	 * Constructs a new registry instance for given super class.
	 * <p>
	 * 
	 * @param superClass is the runtime class type corresponding to the declared parameter type
	 *        {@code T}.
	 * @param packages are the packages in which to search for subclass implementations.
	 * @since 0.07
	 */
	public ConstructorRegistry(Class<? super T> superClass, String... packages)
	{
		_superClass = superClass;
		_nInitialPackages = packages.length;
		_packages = new ArrayList<String>();
		for (String packageName : packages)
		{
			_packages.add(packageName);
		}

		_nameToConstructors = ArrayListMultimap.create();
	}

	/*-------------
	 * Map methods
	 */

	@NonNullByDefault(false)
	@Override
	public boolean containsKey(Object simpleClassName)
	{
		return _nameToConstructors.containsKey(simpleClassName);
	}

	@Override
	@NonNullByDefault(false)
	public boolean containsValue(Object value)
	{
		return _nameToConstructors.containsValue(value);
	}

	@Override
	public Set<Map.Entry<String, Constructor<T>>> entrySet()
	{
		return Collections.unmodifiableSet(new HashSet<>(_nameToConstructors.entries()));
	}

	/**
	 * Looks up no-argument constructor for named class.
	 * <p>
	 * If {@code className} is a fully qualified name referring to a class with an accessible constructor
	 * that takes no arguments, that constructor will be returned. Otherwise, this will
	 * searches all of the registry's packages (see {@link #getPackages()}) for class with given
	 * {@code className} and compatible constructor. Returns first match or null if none is found.
	 * <p>
	 * @param className is either the unqualified or fully qualified name of the class whose constructor is sought.
	 * @see #get(String, Class[])
	 */
	@NonNullByDefault(false)
	@Override
	public final @Nullable Constructor<T> get(Object className)
	{
		if (className instanceof String)
		{
			return get((String)className, ArrayUtil.EMPTY_CLASS_ARRAY);
		}
		
		return null;
	}
	
	@Override
	public Set<String> keySet()
	{
		return Collections.unmodifiableSet(_nameToConstructors.keySet());
	}

	@Override
	public int size()
	{
		return _nameToConstructors.size();
	}

	@Override
	public Collection<Constructor<T>> values()
	{
		return Collections.unmodifiableCollection(_nameToConstructors.values());
	}

	/*---------------
	 * Local methods
	 */

	/**
	 * Adds entry for specified class.
	 * <p>
	 * 
	 * @param newClass
	 * @throws IllegalArgumentException if class is not a subclass of registry's
	 *         {@linkplain #getSuperClass() super class} or if it does not have a publicly
	 *         accessible constructor that takes no arguments.
	 * @since 0.07
	 * @see #addPackage
	 */
	public void addClass(Class<?> newClass)
	{
		final String name = newClass.getSimpleName();

		if (!_superClass.isAssignableFrom(newClass))
		{
			throw new IllegalArgumentException(String.format("%s is not a subclass of %s",
				name, _superClass.getSimpleName()));
		}
		
		if (addConstructorsFrom(newClass, true).isEmpty())
		{
			throw new IllegalArgumentException(String.format("%s does not have an accessible constructor",
				name));
		}
	}

	/**
	 * Adds a package to search for class implementations.
	 * <p>
	 * 
	 * @param packageName is a fully qualified Java package name expected to contain subclasses of
	 *        declared superclass {@code T}.
	 * @see #get(Object)
	 * @since 0.07
	 * @see #addClass
	 * @see #getPackages()
	 */
	public synchronized void addPackage(String packageName)
	{
		_packages.add(packageName);
	}

	/**
	 * Looks up constructor for named class with specified formal parameters.
	 * <p>
	 * If {@code className} is a fully qualified name referring to a class with an accessible constructor
	 * that takes no arguments, that constructor will be returned. Otherwise, this will
	 * searches all of the registry's packages (see {@link #getPackages()}) for class with given
	 * {@code className} and compatible constructor. Returns first match or null if none is found.
	 * <p>
	 * @param className is either the unqualified or fully qualified name of the class whose constructor is sought.
	 * @param formalParameters are the declared types of the parameters to the constructor. Note that these must match
	 * exactly.
	 * @see #get(String, Class[])
	 */
	public final @Nullable Constructor<T> get(String className, Class<?>[] formalParameters)
	{
		List<Constructor<T>> constructors = getAll(className);

		for (Constructor<T> constructor : constructors)
		{
			if (Arrays.equals(formalParameters, constructor.getParameterTypes()))
			{
				return constructor;
			}
		}
		
		return null;
	}
	

	/**
	 * Returns list of all public constructors for class with given name.
	 * @param className is either a simple or fully qualified class name. If a simple name and the class
	 * has not already been loaded, this will search {@link #getPackages()} for a match.
	 * @return non-null list of constructors, which may be empty.
	 * @see #get
	 * @since 0.07
	 */
	public List<Constructor<T>> getAll(String className)
	{
		String name = className;
		
		List<Constructor<T>> constructors = Collections.emptyList();
		
		synchronized(this)
		{
			constructors = _nameToConstructors.get(name);
		}

		if (constructors.isEmpty())
		{
			ClassLoader loader = getClass().getClassLoader();

			if (name.indexOf('.') > 0)
			{
				// Looks like it is qualified with a package name.
				try
				{
					constructors = addConstructorsFrom(Class.forName(name, false, loader), false);
				}
				catch (Throwable e)
				{
				}
			}
			
			// Search packages for a matching class
			for (String packageName : _packages)
			{
				String fullQualifiedName = packageName + "." + name;
				try
				{
					constructors = addConstructorsFrom(Class.forName(fullQualifiedName, false, loader), true);
					if (!constructors.isEmpty())
					{
						break;
					}
				}
				catch (Throwable e)
				{
				}
			}
		}

		return constructors;
	}
	
	/**
	 * Returns class type named by {@code simpleClassName}.
	 * <p>
	 * Simply returns {@linkplain Constructor#getDeclaringClass() declaring class} of value returned
	 * by {@link #get}.
	 * 
	 * @throws RuntimeException if no such class can be found.
	 * @since 0.07
	 */
	public Class<? extends T> getClass(String simpleClassName)
	{
		Class<? extends T> c = getClassOrNull(simpleClassName);
		if (c == null)
		{
			throw noMatchingClass(simpleClassName);
		}
		return c;
	}

	/**
	 * Returns class type named by {@code simpleClassName}.
	 * <p>
	 * Simply returns {@linkplain Constructor#getDeclaringClass() declaring class} of value returned
	 * by {@link #get} or else null.
	 * 
	 * @since 0.07
	 */
	@Nullable
	public Class<? extends T> getClassOrNull(String simpleClassName)
	{
		List<Constructor<T>> constructors = getAll(simpleClassName);
		if (!constructors.isEmpty())
		{
			return constructors.get(0).getDeclaringClass();
		}
		return null;
	}

	/**
	 * Returns copy of list of package names searched by this registry.
	 * <p>
	 * 
	 * @since 0.07
	 * @see #addPackage(String)
	 * @see #get(Object)
	 */
	public synchronized String[] getPackages()
	{
		return _packages.toArray(new String[_packages.size()]);
	}

	/**
	 * Class type of declared type {@code T}.
	 * 
	 * @since 0.07
	 */
	public Class<? super T> getSuperClass()
	{
		return _superClass;
	}

	/**
	 * Instantiates an instance of named class using no-argument constructor.
	 * <p>
	 * Simply invokes {@link Constructor#newInstance} on constructor returned by {@link #get}.
	 * 
	 * @throws RuntimeException if no such class can be found.
	 * @since 0.07
	 */
	public T instantiate(String simpleClassName)
	{
		T instance = instantiateOrNull(simpleClassName);
		if (instance == null)
		{
			throw noMatchingClass(simpleClassName);
		}
		return instance;
	}

	/**
	 * Instantiates an instance of named class using no-argument constructor.
	 * <p>
	 * Simply invokes {@link Constructor#newInstance} on constructor returned by {@link #get} or
	 * else returns null.
	 * 
	 * @since 0.07
	 */
	@Nullable
	public T instantiateOrNull(String simpleClassName)
	{
		Constructor<T> constructor = get(simpleClassName);
		if (constructor != null)
		{
			try
			{
				return constructor.newInstance();
			}
			catch (InvocationTargetException ex)
			{
				throw new RuntimeException(ex.getCause());
			}
			catch (ReflectiveOperationException ex)
			{
				throw new RuntimeException(ex);
			}
		}

		return null;
	}

	/**
	 * Preloads all subclasses of declared superclass {@code T} found in registry's packages.
	 * <p>
	 * Searches all of the packages in {@link #getPackages()} for subclasses of {@code T} and adds
	 * then to the registry.
	 * <p>
	 * 
	 * @since 0.07
	 */
	public synchronized void loadAll()
	{
		ClassLoader loader = getClass().getClassLoader();

		ClassPath path;
		try
		{
			path = ClassPath.from(loader);
		}
		catch (IOException ex)
		{
			throw new RuntimeException(ex);
		}
		for (String packageName : _packages)
		{
			for (ClassPath.ClassInfo info : path.getTopLevelClasses(packageName))
			{
				addConstructorsFrom(info.load(), true);
			}
		}
	}

	/**
	 * Resets back to initial state upon construction.
	 * <p>
	 * @since 0.08
	 */
	public synchronized void reset()
	{
		// Remove any added packages
		_packages.subList(_nInitialPackages, _packages.size()).clear();
		// Clear constructor cache
		_nameToConstructors.clear();
	}
	
	/*-----------------
	 * Private methods
	 */
	
	@NonNullByDefault(false)
	private static enum ConstructorComparator implements Comparator<Constructor<?>>
	{
		INSTANCE;

		@Override
		public int compare(Constructor<?> c1, Constructor<?> c2)
		{
			return Integer.compare(c1.getParameterTypes().length, c2.getParameterTypes().length);
		}
	}

	/**
	 * Adds public constructors from given type indexed by qualified name
	 * @param addSimpleName if true, then constructors will also be indexed by the classes simple name.
	 * @return the constructors that were added
	 * @since 0.07
	 */
	@SuppressWarnings("unchecked")
	private synchronized List<Constructor<T>> addConstructorsFrom(Class<?> type, boolean addSimpleName)
	{
		List<Constructor<T>> constructors = Collections.emptyList();
		
		int modifiers = type.getModifiers();
		if (Modifier.isPublic(modifiers) && !Modifier.isAbstract(modifiers) && _superClass.isAssignableFrom(type))
		{
			try
			{
				final Constructor<T>[] array = (Constructor<T>[]) type.getConstructors();
				Arrays.sort(array, ConstructorComparator.INSTANCE);
				constructors = Arrays.asList(array);
				if (addSimpleName)
				{
					_nameToConstructors.putAll(type.getSimpleName(), constructors);
				}
				_nameToConstructors.putAll(type.getCanonicalName(), constructors);
			}
			catch (SecurityException ex)
			{
				// Ignore
			}
		}
		
		return constructors;
	}
	
	protected RuntimeException noMatchingClass(String simpleClassName)
	{
		return new RuntimeException(String.format(
			"Cannot find class named '%s' with accessible constructor with appropriate signature", simpleClassName));
	}

}