#region License

/*
 * Copyright 2002-2004 the original author or authors.
 *
 * 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.
 */

#endregion

#region Imports

using System;
using System.Collections;
using System.Reflection;

using Common.Logging;
using Spring.Util;
using Spring.Core.TypeResolution;
using Spring.Core;

#endregion

namespace Spring.Transaction.Interceptor
{
	/// <summary>
	/// Simple implementation of the <see cref="Spring.Transaction.Interceptor.ITransactionAttributeSource"/>
	/// interface that allows attributes to be stored per method in a map.
	/// </summary>
	/// <author>Rod Johnson</author>
	/// <author>Juergen Hoeller</author>
	/// <author>Griffin Caprio (.NET)</author>
	public class MethodMapTransactionAttributeSource : ITransactionAttributeSource
	{
		private IDictionary _methodMap;
		private IDictionary _nameMap;

	    #region Logging Definition

	    private static readonly ILog LOG = LogManager.GetLogger(typeof (MethodMapTransactionAttributeSource));

	    #endregion

	    /// <summary>
		/// Creates a new instance of the
		/// <see cref="Spring.Transaction.Interceptor.MethodMapTransactionAttributeSource"/> class.
		/// </summary>
		public MethodMapTransactionAttributeSource()
		{
			_methodMap = new Hashtable();
			_nameMap = new Hashtable();
		}

		/// <summary>
		/// Set a name/attribute map, consisting of "FQCN.method, AssemblyName" method names
		/// (e.g. "MyNameSpace.MyClass.MyMethod, MyAssembly") and ITransactionAttribute
		/// instances (or Strings to be converted to <see cref="ITransactionAttribute"/> instances).
		/// </summary>
		/// <remarks>
		/// <p>
		/// The key values of the supplied <see cref="System.Collections.IDictionary"/>
		/// must adhere to the following form:<br/>
		/// <code>FQCN.methodName, Assembly</code>.
		/// </p>
		/// <example>
        /// <code>MyNameSpace.MyClass.MyMethod, MyAssembly</code>
		///	</example>
		/// </remarks>
		public IDictionary MethodMap
		{
			set
			{
			    foreach (DictionaryEntry entry in value)
			    {

			        string name = entry.Key as string;
                    ITransactionAttribute attribute = null;
                    //Check if we need to convert from a string to ITransactionAttribute
                    if (entry.Value is ITransactionAttribute)
                    {
                        attribute = entry.Value as ITransactionAttribute;
                    }
                    else
                    {
                        TransactionAttributeEditor editor = new TransactionAttributeEditor();
                        editor.SetAsText(entry.Value.ToString());
                        attribute = editor.Value;
                    }
                    AddTransactionalMethod( name, attribute );                    
			        
			    }

			} 
		}
		/// <summary>
		/// Add an attribute for a transactional method.
		/// </summary>
		/// <remarks>
		/// Method names can end or start with "*" for matching multiple methods.
		/// </remarks>
		/// <param name="name">The class and method name, separated by a dot.</param>
		/// <param name="attribute">The attribute to be associated with the method.</param>
		public void AddTransactionalMethod( string name, ITransactionAttribute attribute )
		{
            AssertUtils.ArgumentNotNull(name, "name");
            int lastCommaIndex = name.LastIndexOf( "," );
            if (lastCommaIndex == -1)
            {
                throw new ArgumentException("'" + name + "'" +
                                            " is not a valid method name, missing AssemblyName.  Format is FQN.MethodName, AssemblyName.");                
            }
            string fqnWithMethod = name.Substring(0, lastCommaIndex);

			int lastDotIndex = fqnWithMethod.LastIndexOf( "." );
			if ( lastDotIndex == -1 )
			{
				throw new TransactionUsageException("'" + fqnWithMethod + "' is not a valid method name: format is FQN.methodName");
			}
			string className = fqnWithMethod.Substring(0, lastDotIndex );
            string assemblyName = name.Substring(lastCommaIndex + 1);
			string methodName = fqnWithMethod.Substring(lastDotIndex + 1 );
            string fqnClassName = className + ", " + assemblyName;
			try
			{
				Type type = TypeResolutionUtils.ResolveType(fqnClassName);
				AddTransactionalMethod( type, methodName, attribute );
			} catch ( TypeLoadException exception )
			{
				throw new TransactionUsageException( "Type '" + fqnClassName + "' not found.", exception );
			} 
		}

		/// <summary>
		/// Add an attribute for a transactional method.
		/// </summary>
		/// <remarks>
		/// Method names can end or start with "*" for matching multiple methods.
		/// </remarks>
		/// <param name="type">The target interface or class.</param>
		/// <param name="mappedName">The mapped method name.</param>
		/// <param name="transactionAttribute">
		/// The attribute to be associated with the method.
		/// </param>
		public void AddTransactionalMethod(
			Type type, string mappedName, ITransactionAttribute transactionAttribute )
		{
            // TODO address method overloading? At present this will
            // simply match all methods that have the given name.
			string name = type.Name + "." + mappedName;
			MethodInfo[] methods = type.GetMethods();
			IList matchingMethods = new ArrayList();
			for ( int i = 0; i < methods.Length; i++ )
			{
				if ( methods[i].Name.Equals( mappedName ) || IsMatch(methods[i].Name, mappedName ))
				{
					matchingMethods.Add( methods[i] );
				}
			}
			if ( matchingMethods.Count == 0 )
			{
				throw new TransactionUsageException("Couldn't find method '" + mappedName + "' on Type [" + type.Name + "]");
			}
            // register all matching methods
			foreach ( MethodInfo currentMethod in matchingMethods )
			{
				string regularMethodName = (string)_nameMap[currentMethod];
				if ( regularMethodName == null || ( !regularMethodName.Equals(name) && regularMethodName.Length <= name.Length))
				{
                    // No already registered method name, or more specific
                    // method name specification now -> (re-)register method.
                    if (LOG.IsDebugEnabled && regularMethodName != null) 
                    {
                        LOG.Debug("Replacing attribute for transactional method [" + currentMethod + "]: current name '" +
                            name + "' is more specific than '" + regularMethodName + "'");
                    }
					_nameMap.Add( currentMethod, name );
					AddTransactionalMethod( currentMethod, transactionAttribute );
				}
                else
				{
                    if (LOG.IsDebugEnabled && regularMethodName != null) 
                    {
                        LOG.Debug("Keeping attribute for transactional method [" + currentMethod + "]: current name '" +
                            name + "' is not more specific than '" + regularMethodName + "'");
                    }
				}
			}
		}

		/// <summary>
		/// Add an attribute for a transactional method.
		/// </summary>
		/// <param name="method">The transactional method.</param>
		/// <param name="transactionAttribute">
		/// The attribute to be associated with the method.
		/// </param>
		public void AddTransactionalMethod( MethodInfo method, ITransactionAttribute transactionAttribute )
		{
			_methodMap.Add( method, transactionAttribute );
		}

		/// <summary>
		/// Does the supplied <paramref name="methodName"/> match the supplied <paramref name="mappedName"/>?
		/// </summary>
		/// <remarks>
		/// The default implementation checks for "xxx*", "*xxx" and "*xxx*" matches,
	    /// as well as direct equality.  This behaviour can (of course) be overridden in 
	    /// derived classes.
		/// </remarks>
		/// <param name="methodName">The method name of the class.</param>
		/// <param name="mappedName">The name in the descriptor.</param>
		/// <returns><b>True</b> if the names match.</returns>
		protected virtual bool IsMatch( string methodName, string mappedName )
		{
            return PatternMatchUtils.SimpleMatch(mappedName, methodName);
		}

		#region ITransactionAttributeSource Members
		/// <summary>
		/// Return the <see cref="Spring.Transaction.Interceptor.ITransactionAttribute"/> for this
		/// method.
		/// </summary>
		/// <param name="method">The method to check.</param>
		/// <param name="targetType">
		/// The target <see cref="System.Type"/>. May be null, in which case the declaring 
		/// class of the supplied <paramref name="method"/> must be used.
		/// </param>
		/// <returns>
		/// A <see cref="Spring.Transaction.Interceptor.ITransactionAttribute"/> or 
		/// null if the method is non-transactional.
		/// </returns>
		public ITransactionAttribute ReturnTransactionAttribute(MethodInfo method, Type targetType)
		{
            //Might have registered MethodInfo objects whose declaring type is the interface, so 'downcast'
            //to the most specific method which is typically what is passed in as the first method argument.
            foreach (DictionaryEntry dictionaryEntry in _methodMap)
            {
                MethodInfo currentMethod = (MethodInfo)dictionaryEntry.Key;
                
                MethodInfo specificMethod;
                if (targetType == null)
                {
                    specificMethod = currentMethod;
                }
                else
                {
                    ParameterInfo[] parameters = currentMethod.GetParameters();

                    ComposedCriteria searchCriteria = new ComposedCriteria();
                    searchCriteria.Add(new MethodNameMatchCriteria(currentMethod.Name));
                    searchCriteria.Add(new MethodParametersCountCriteria(parameters.Length));
#if NET_2_0
                    searchCriteria.Add(new MethodGenericArgumentsCountCriteria(
                        currentMethod.GetGenericArguments().Length));
#endif
                    searchCriteria.Add(new MethodParametersCriteria(ReflectionUtils.GetParameterTypes(parameters)));

                    MemberInfo[] matchingMethods = targetType.FindMembers(
                        MemberTypes.Method,
                        BindingFlags.Instance | BindingFlags.Public,
                        new MemberFilter(new CriteriaMemberFilter().FilterMemberByCriteria),
                        searchCriteria);

                    if (matchingMethods != null && matchingMethods.Length == 1)
                    {
                        specificMethod = matchingMethods[0] as MethodInfo;
                    }
                    else
                    {
                        specificMethod = currentMethod;
                    }
                }

                if (method == specificMethod)
                {
                    return (ITransactionAttribute)dictionaryEntry.Value;
                }
            }
            return (ITransactionAttribute)_methodMap[method];
		}
		#endregion

	
	}
}
