/*******************************************************************************
*   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.options;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

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

import org.eclipse.jdt.annotation.Nullable;

import com.google.common.collect.AbstractIterator;
import com.google.common.collect.ObjectArrays;

/**
 * A registry of known option keys indexed by qualified name.
 * <p>
 * For use in looking up option keys by string either for use in configuration files or in
 * implementing external APIs (e.g. MATLAB).
 * <p>
 * @since 0.07
 */
@ThreadSafe
public class OptionRegistry implements Iterable<IOptionKey<?>>
{
	/*-------
	 * State
	 */
	
	/**
	 * Maps canonical class names to {@link OptionKeys}.
	 * <p>
	 * Writes are protected by lock. Reads are not.
	 */
	@GuardedBy("this")
	private final ConcurrentNavigableMap<String, OptionKeys> _canonicalMap;
	
	/**
	 * Maps simple class names to {@link OptionKeys}
	 * <p>
	 * Unlike {@link #_canonicalMap}, this may map name to keys for multiple
	 * classes with the same simple name.
	 * <p>
	 * Writes are protected by lock. Reads are not.
	 */
	@GuardedBy("this")
	private final ConcurrentNavigableMap<String, OptionKeys[]> _simpleMap;
	
	private final boolean _autoLoad;
	
	@GuardedBy("this")
	private volatile int _size;
	
	private static final int publicStatic = Modifier.PUBLIC | Modifier.STATIC;

	/*--------------
	 * Construction
	 */
	
	/**
	 * Create a new empty registry.
	 * <p>
	 * @param autoLoad determines setting of {@link #autoLoadKeys()}.
	 * @since 0.07
	 */
	public OptionRegistry(boolean autoLoad)
	{
		_autoLoad = autoLoad;
		_canonicalMap = new ConcurrentSkipListMap<String, OptionKeys>();
		_simpleMap = new ConcurrentSkipListMap<String,OptionKeys[]>();
	}
	
	/**
	 * Create a new empty registry with autoloading enabled.
	 * @see #OptionRegistry(boolean)
	 * @since 0.07
	 */
	public OptionRegistry()
	{
		this(true);
	}
	
	/*------------------
	 * Iterable methods
	 */
	
	/**
	 * Iterates over option keys that are currently registered with this object in
	 * order of {@linkplain OptionKey#canonicalName(IOptionKey) canonical name}.
	 */
	@Override
	public Iterator<IOptionKey<?>> iterator()
	{
		return new OptionIterator();
	}
	
	private class OptionIterator extends AbstractIterator<IOptionKey<?>>
	{
		private final Iterator<OptionKeys> _keysIterator;
		private volatile Iterator<IOptionKey<?>> _keyIterator;
		
		private OptionIterator()
		{
			_keysIterator = _canonicalMap.values().iterator();
			_keyIterator = Collections.emptyIterator();
		}
		
		@Override
		protected @Nullable IOptionKey<?> computeNext()
		{
			Iterator<IOptionKey<?>> keyIter = _keyIterator;
				
			while (!keyIter.hasNext())
			{
				if (_keysIterator.hasNext())
				{
					_keyIterator = keyIter = _keysIterator.next().values().iterator();
				}
				else
				{
					endOfData();
					return null;
				}
			}
			
			return keyIter.next();
		}
	}
	
	/*------------------------
	 * OptionRegistry methods
	 */
	
	/**
	 * Adds option keys to registry.
	 * <p>
	 * @return true if registry was changed (keys were not already in registry).
	 * @since 0.07
	 */
	public boolean add(OptionKeys keys)
	{
		Class<?> declaringClass = keys.declaringClass();
		
		synchronized (this)
		{
			if (null != _canonicalMap.put(declaringClass.getCanonicalName(), keys))
			{
				return false;
			}
			
			OptionKeys[] array = new OptionKeys[] { keys };
			final String classname = declaringClass.getSimpleName();
			
			OptionKeys[] existingArray = _simpleMap.get(classname);
			if (existingArray == null)
			{
				_simpleMap.put(classname, array);
			}
			else
			{
				_simpleMap.put(classname, ObjectArrays.concat(existingArray, keys));
			}
			
			_size += keys.values().size();
		}

		return true;
	}
	
	/**
	 * Registers statically declared option key instances found reflectively in specified classes.
	 * Adds all statically declared, final fields of type {@link IOptionKey} whose {@linkplain IOptionKey#name() name}
	 * attribute matches its declared name. Also will recursively add from public nested classes.
	 * 
	 * @return the number of unique option keys that were added
	 * @since 0.07
	 */
	public int addFromClasses(Class<?> ... declaringClasses)
	{
		int nAdded = 0;
		
		for (Class<?> declaringClass : declaringClasses)
		{
			OptionKeys keys = OptionKeys.declaredInClass(declaringClass);
			int nAddedFromClass = keys.values().size();

			if (nAddedFromClass > 0 && !add(keys))
			{
				nAddedFromClass = 0;
			}

			for (Class<?> innerClass : declaringClass.getDeclaredClasses())
			{
				if ((innerClass.getModifiers() & publicStatic) == publicStatic)
				{
					nAddedFromClass += addFromClasses(innerClass);
				}
			}
			
			nAdded += nAddedFromClass;
		}
		
		return nAdded;
	}
	
	/**
	 * Returns key for given qualified name or throws an error.
	 * <p>
	 * @param keyOrName either a {@link IOptionKey} instance which will simply be returned or
	 * a {@link String} compatible with {@link #get(String)}.
	 * @throws NoSuchElementException if key not found or input argument is not the correct type.
	 * @throws IllegalArgumentException if {@code keyOrName} does not have the correct type.
	 * @since 0.07
	 */
	public IOptionKey<?> asKey(Object keyOrName)
	{
		IOptionKey<?> key;
		
		if (keyOrName instanceof String)
		{
			String name = (String)keyOrName;
			key = get(name);
			if (key == null)
			{
				throw new NoSuchElementException(String.format("Unknown option key '%s'", name));
			}
			else
			{
				return key;
			}
		}
		else if (keyOrName instanceof IOptionKey)
		{
			key = (IOptionKey<?>)keyOrName;
		}
		else
		{
			throw new IllegalArgumentException(
				String.format("Expected String or IOptionKey instead of '%s'",
					keyOrName.getClass().getSimpleName()));
		}
		
		return key;
	}

	/**
	 * If true, registry will attempt to automatically load fully qualified keys from classes.
	 * <p>
	 * For instance, if attempting to get key for the string:
	 * 
	 * <blockquote>
	 * "com.mycompany.MyOptions.myOption"
	 * </blockquote>
	 * 
	 * the registry will automatically load all option keys declared in the class {@code MyOptions}.
	 * This will only work for fully qualified names whose classes are on the class path.
	 * <p>
	 * This attribute is set in the constructor.
	 * @since 0.07
	 */
	public boolean autoLoadKeys()
	{
		return _autoLoad;
	}
	
	/**
	 * Returns option key with specified name or null if not found.
	 * <p>
	 * If {@link #autoLoadKeys()} is enabled and {@code name} is a full canonical name then
	 * the key will be loaded automatically by loading the class and adding all of its declared
	 * options.
	 * <p>
	 * @param name is either the {@linkplain OptionKey#canonicalName(IOptionKey) canonical name} or
	 * {@linkplain OptionKey#qualifiedName(IOptionKey) qualified name} of the option.
	 * @throws AmbiguousOptionNameException if more than one possible option key matches
	 * the given name. This will not happen if a canonical name is used.
	 * The list of possible matches can be found in the exception.
	 * @see OptionKey#canonicalName(IOptionKey)
	 * @since 0.07
	 */
	public @Nullable IOptionKey<?> get(String name)
	{
		int i = name.lastIndexOf('.');
	
		if (i >= 0)
		{
			String className = name.substring(0, i);
			String optionName = name.substring(i + 1);

			if (className.indexOf('.') >= 0)
			{
				// Should be canonical class name
				OptionKeys keys = _canonicalMap.get(className);

				if (keys == null && _autoLoad)
				{
					try
					{
						Class<?> c = Class.forName(className, false, getClass().getClassLoader());
						add(keys = OptionKeys.declaredInClass(c));
					}
					catch (ClassNotFoundException ex)
					{
					}
				}
				
				if (keys != null)
				{
					return keys.get(optionName);
				}
			}
			else
			{
				OptionKeys[] keysArray = _simpleMap.get(className);

				if (keysArray != null)
				{
					if (keysArray.length == 1)
					{
						return keysArray[0].get(optionName);
					}
					else
					{
						ArrayList<IOptionKey<?>> options = new ArrayList<IOptionKey<?>>();
						for (OptionKeys keys : keysArray)
						{
							IOptionKey<?> option = keys.get(optionName);
							if (option != null)
							{
								options.add(option);
							}
						}
						
						switch (options.size())
						{
						case 0:
							break;
						case 1:
							return options.get(0);
						default:
							throw new AmbiguousOptionNameException(name, options);
						}
					}
				}
			}
		}

		return null;
	}

	/**
	 * Returns a newly constructed list of keys matching regular expression.
	 * <p>
	 * The will contain all keys whose {@linkplain OptionKey#qualifiedName(IOptionKey) qualified name} matches the
	 * given regular expression pattern sorted by name.
	 * @since 0.07
	 */
	public ArrayList<IOptionKey<?>> getAllMatching(Pattern pattern)
	{
		Matcher matcher = pattern.matcher("");
		ArrayList<IOptionKey<?>> results = new ArrayList<IOptionKey<?>>();
		
		for (IOptionKey<?> key : this)
		{
			matcher.reset(OptionKey.qualifiedName(key));
			if (matcher.matches())
			{
				results.add(key);
			}
		}
		
		return results;
	}
	
	/**
	 * Returns a newly constructed list of keys matching regular expression.
	 * <p>
	 * The will contain all keys whose {@linkplain OptionKey#qualifiedName(IOptionKey) qualified name} matches the
	 * given regular expression pattern sorted by name.
	 * @since 0.07
	 */
	public ArrayList<IOptionKey<?>> getAllMatching(String regexp)
	{
		return getAllMatching(Pattern.compile(regexp));
	}
	
	/**
	 * Iterates over {@link OptionKeys} registered with this object.
	 * @since 0.07
	 */
	public Collection<OptionKeys> getOptionKeys()
	{
		return Collections.unmodifiableCollection(_canonicalMap.values());
	}
	
	/**
	 * The number of entries in the registry.
	 * <p>
	 * @since 0.07
	 */
	public int size()
	{
		return _size;
	}

}
