// Copyright (c) Rotorz Limited. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root.

using System;
using System.Collections.Generic;
using System.Linq;

namespace HutongGames.Rotorz.ReorderableList {

	/// <summary>
	/// Provides meta information which is useful when creating new implementations of
	/// the <see cref="IElementAdderMenuBuilder{TContext}"/> interface.
	/// </summary>
	public static class ElementAdderMeta {

		#region Adder Menu Command Types

		private static Dictionary<Type, Dictionary<Type, List<Type>>> s_ContextMap = new Dictionary<Type, Dictionary<Type, List<Type>>>();

		private static IEnumerable<Type> GetMenuCommandTypes<TContext>() {
			return
				from a in AppDomain.CurrentDomain.GetAssemblies()
				from t in a.GetTypes()
				where t.IsClass && !t.IsAbstract && t.IsDefined(typeof(ElementAdderMenuCommandAttribute), false)
				where typeof(IElementAdderMenuCommand<TContext>).IsAssignableFrom(t)
				select t;
		}

		/// <summary>
		/// Gets an array of the <see cref="IElementAdderMenuCommand{TContext}"/> types
		/// that are associated with the specified <paramref name="contractType"/>.
		/// </summary>
		/// <typeparam name="TContext">Type of the context object that elements can be added to.</typeparam>
		/// <param name="contractType">Contract type of addable elements.</param>
		/// <returns>
		/// An array containing zero or more <see cref="System.Type"/>.
		/// </returns>
		/// <exception cref="System.ArgumentNullException">
		/// If <paramref name="contractType"/> is <c>null</c>.
		/// </exception>
		/// <seealso cref="GetMenuCommands{TContext}(Type)"/>
		public static Type[] GetMenuCommandTypes<TContext>(Type contractType) {
			if (contractType == null)
				throw new ArgumentNullException("contractType");

			Dictionary<Type, List<Type>> contractMap;
			List<Type> commandTypes;
			if (s_ContextMap.TryGetValue(typeof(TContext), out contractMap)) {
				if (contractMap.TryGetValue(contractType, out commandTypes))
					return commandTypes.ToArray();
			}
			else {
				contractMap = new Dictionary<Type, List<Type>>();
				s_ContextMap[typeof(TContext)] = contractMap;
			}

			commandTypes = new List<Type>();

			foreach (var commandType in GetMenuCommandTypes<TContext>()) {
				var attributes = (ElementAdderMenuCommandAttribute[])Attribute.GetCustomAttributes(commandType, typeof(ElementAdderMenuCommandAttribute));
				if (!attributes.Any(a => a.ContractType == contractType))
					continue;

				commandTypes.Add(commandType);
			}

			contractMap[contractType] = commandTypes;
			return commandTypes.ToArray();
		}

		/// <summary>
		/// Gets an array of <see cref="IElementAdderMenuCommand{TContext}"/> instances
		/// that are associated with the specified <paramref name="contractType"/>.
		/// </summary>
		/// <typeparam name="TContext">Type of the context object that elements can be added to.</typeparam>
		/// <param name="contractType">Contract type of addable elements.</param>
		/// <returns>
		/// An array containing zero or more <see cref="IElementAdderMenuCommand{TContext}"/> instances.
		/// </returns>
		/// <exception cref="System.ArgumentNullException">
		/// If <paramref name="contractType"/> is <c>null</c>.
		/// </exception>
		/// <seealso cref="GetMenuCommandTypes{TContext}(Type)"/>
		public static IElementAdderMenuCommand<TContext>[] GetMenuCommands<TContext>(Type contractType) {
			var commandTypes = GetMenuCommandTypes<TContext>(contractType);
			var commands = new IElementAdderMenuCommand<TContext>[commandTypes.Length];
			for (int i = 0; i < commandTypes.Length; ++i)
				commands[i] = (IElementAdderMenuCommand<TContext>)Activator.CreateInstance(commandTypes[i]);
			return commands;
		}

		#endregion

		#region Concrete Element Types

		private static Dictionary<Type, Type[]> s_ConcreteElementTypes = new Dictionary<Type, Type[]>();

		private static IEnumerable<Type> GetConcreteElementTypesHelper(Type contractType) {
			if (contractType == null)
				throw new ArgumentNullException("contractType");

			Type[] concreteTypes;
			if (!s_ConcreteElementTypes.TryGetValue(contractType, out concreteTypes)) {
				concreteTypes =
					(from a in AppDomain.CurrentDomain.GetAssemblies()
					 from t in a.GetTypes()
					 where t.IsClass && !t.IsAbstract && contractType.IsAssignableFrom(t)
					 orderby t.Name
					 select t
					).ToArray();
				s_ConcreteElementTypes[contractType] = concreteTypes;
			}

			return concreteTypes;
		}

		/// <summary>
		/// Gets a filtered array of the concrete element types that implement the
		/// specified <paramref name="contractType"/>.
		/// </summary>
		/// <remarks>
		/// <para>A type is excluded from the resulting array when one or more of the
		/// specified <paramref name="filters"/> returns a value of <c>false</c>.</para>
		/// </remarks>
		/// <param name="contractType">Contract type of addable elements.</param>
		/// <param name="filters">An array of zero or more filters.</param>
		/// <returns>
		/// An array of zero or more concrete element types.
		/// </returns>
		/// <exception cref="System.ArgumentNullException">
		/// If <paramref name="contractType"/> is <c>null</c>.
		/// </exception>
		/// <seealso cref="GetConcreteElementTypes(Type)"/>
		public static Type[] GetConcreteElementTypes(Type contractType, Func<Type, bool>[] filters) {
			return
				(from t in GetConcreteElementTypesHelper(contractType)
				 where IsTypeIncluded(t, filters)
				 select t
				).ToArray();
		}

		/// <summary>
		/// Gets an array of all the concrete element types that implement the specified
		/// <paramref name="contractType"/>.
		/// </summary>
		/// <param name="contractType">Contract type of addable elements.</param>
		/// <returns>
		/// An array of zero or more concrete element types.
		/// </returns>
		/// <exception cref="System.ArgumentNullException">
		/// If <paramref name="contractType"/> is <c>null</c>.
		/// </exception>
		/// <seealso cref="GetConcreteElementTypes(Type, Func{Type, bool}[])"/>
		public static Type[] GetConcreteElementTypes(Type contractType) {
			return GetConcreteElementTypesHelper(contractType).ToArray();
		}

		private static bool IsTypeIncluded(Type concreteType, Func<Type, bool>[] filters) {
			if (filters != null)
				foreach (var filter in filters)
					if (!filter(concreteType))
						return false;
			return true;
		}

		#endregion

	}

}
