#region License
// Copyright (c) 2004 Griffin Caprio & Choy Rim. All rights reserved.
#endregion
#region Imports
using System;
using System.Collections;
using System.Reflection;
using System.Reflection.Emit;
using DotNetMock.Dynamic;
#endregion

namespace DotNetMock.Dynamic.Generate
{
	/// <summary>
	/// Builds mock classes dynamically.
	/// </summary>
	/// <author>Griffin Caprio</author>
	/// <author>Choy Rim</author>
	public class MockClassBuilder 
	{
		private const string HANDLER_FIELD_NAME = "_mockedCallHandler";
		private static readonly Type HANDLER_TYPE = typeof(IMockedCallHandler);
		private static readonly MethodInfo CALL_HANDLING_METHOD =
			HANDLER_TYPE.GetMethod(
			"Call",
			new Type[] { typeof(MethodInfo), typeof(object[]) }
			);

		TypeBuilder _typeBuilder;
		FieldBuilder _handlerFieldBuilder;
		Type _mockClass = null;

		/// <summary>
		/// Create instance of mock class builder.
		/// </summary>
		/// <param name="moduleBuilder"><see cref="ModuleBuilder"/> that
		/// we'll define new type in</param>
		/// <param name="mockClassName">name of mock class that we are
		/// making</param>
		/// <param name="superClass">base class that we are extending
		/// to implement our mock class</param>
		/// <param name="interfaces">interfaces that our mock class
		/// will (declare that we) implement</param>
		public MockClassBuilder(
			ModuleBuilder moduleBuilder,
			string mockClassName, Type superClass, Type[] interfaces
			) 
		{
			_typeBuilder = moduleBuilder.DefineType(
				mockClassName,
				TypeAttributes.Public,
				superClass, interfaces
				);
			_handlerFieldBuilder = _typeBuilder.DefineField(
				HANDLER_FIELD_NAME,
				HANDLER_TYPE,
				FieldAttributes.Public
				);
		}
		/// <summary>
		/// The mock class we've built.
		/// </summary>
		public Type MockClass 
		{
			get {
				if ( ! IsCompiled ) 
				{
					throw new InvalidOperationException(
						"Must compile before accessing the MockClass"
						);
				}
				return _mockClass;
			}
		}
		/// <summary>
		/// The field that holds our <see cref="IMockedCallHandler"/>
		/// which will receive all calls to this type.
		/// </summary>
		public FieldInfo HandlerField 
		{
			get 
			{
				if ( ! IsCompiled ) 
				{
					throw new InvalidOperationException(
						"Must compile before accessing the HandlerField"
						);
				}
				return _mockClass.GetField(HANDLER_FIELD_NAME);
			}
		}
		/// <summary>
		/// Has this class been compiled into a usable type?
		/// </summary>
		public bool IsCompiled 
		{
			get { return _mockClass!=null; }
		}
		/// <summary>
		/// Compile the implemented methods so far into a real
		/// type.
		/// </summary>
		public void Compile() 
		{
			if ( IsCompiled ) 
			{
				throw new InvalidOperationException(
					"Cannot compile class more than once."
					);
			}	
			_mockClass = _typeBuilder.CreateType();
		}
		/// <summary>
		/// Define a mock implementation method on our mock class given
		/// a <see cref="MethodInfo"/>.
		/// </summary>
		/// <param name="mi"><see cref="MethodInfo"/> describing
		/// the method we must mock</param>
		public void ImplementMockedMethod(MethodInfo mi) 
		{
			ImplementMockedMethod(mi.Name, mi.ReturnType, getParameterTypes(mi), mi);
		}
		/// <summary>
		/// Define a mock implementation of a method.
		/// </summary>
		/// <param name="methodName">name of method to mock</param>
		/// <param name="returnType">return <see cref="Type"/> of method
		/// to mock</param>
		/// <param name="parameterTypes">array of <see cref="Type"/>s in
		/// the method signature</param>
		public void ImplementMockedMethod(string methodName, Type returnType, Type[] parameterTypes )
		{
			ImplementMockedMethod(methodName, returnType, parameterTypes, null);
		}
		/// <summary>
		/// Define a mock implementation of a method.
		/// </summary>
		/// <param name="methodName">name of method to mock</param>
		/// <param name="returnType">return <see cref="Type"/> of method
		/// to mock</param>
		/// <param name="parameterTypes">array of <see cref="Type"/>s in
		/// the method signature</param>
		/// <param name="mi">if not null, used to get extra parameter information</param>
		public void ImplementMockedMethod(string methodName, Type returnType, Type[] parameterTypes, MethodInfo mi)
		{
			if ( IsCompiled ) 
			{
				throw new InvalidOperationException(
					"Cannot add methods after class has been compiled."
					);
			}	
			MethodBuilder methodBuilder = _typeBuilder.DefineMethod(
				methodName,
				MethodAttributes.Public | MethodAttributes.Virtual,
				returnType,
				parameterTypes
				);
			if ( mi!=null ) 
			{
				ParameterInfo[] pis = mi.GetParameters();
				for (int i = 0; i<pis.Length; ++i)
				{
					ParameterInfo pi = pis[i];
					methodBuilder.DefineParameter(
						i+1,
						pi.Attributes,
						pi.Name
						);
				}
			}
			ILGenerator il = methodBuilder.GetILGenerator();

			il.DeclareLocal(typeof(object[]));

			il.Emit(OpCodes.Ldarg_0);
			// stack: this
			il.Emit(OpCodes.Ldfld, _handlerFieldBuilder);
			// stack: this handler
			// emit call to get current method
			il.EmitCall(
				OpCodes.Call,
				typeof(MethodBase).GetMethod("GetCurrentMethod", new Type[0]),
				null
				);
			// stack: this handler methodinfo
			// assert parameterTypes.Length<128
			il.Emit(OpCodes.Ldc_I4_S, (sbyte) parameterTypes.Length);
			il.Emit(OpCodes.Newarr, typeof(object));
			il.Emit(OpCodes.Dup);
			// store args array in local var
			il.Emit(OpCodes.Stloc_0);
			// stack: this handler methodinfo args[]

			// fill args array with arguments of call
			if (parameterTypes.Length > 0) 
			{
				for(int i = 0; i < parameterTypes.Length; i++)
				{
					Type parameterType = parameterTypes[i];
					Type elementType = parameterType;
					// push array ref onto stack
					il.Emit(OpCodes.Ldloc_0);
					// push array index onto stack
					il.Emit(OpCodes.Ldc_I4_S, (sbyte) i);
					// load corresponding argument
					il.Emit(OpCodes.Ldarg_S, (sbyte) (i + 1));
					// dereference if necessary
					if ( parameterType.IsByRef ) 
					{
						elementType = parameterType.GetElementType();
						ILUtils.EmitTypedLdind(il, elementType);
					}
					if ( elementType.IsValueType )
					{
						il.Emit(OpCodes.Box, elementType);
					}
					il.Emit(OpCodes.Stelem_Ref);
				}
			}

			il.EmitCall(OpCodes.Callvirt, CALL_HANDLING_METHOD, null);

			if (returnType == typeof(void)) 
			{
				il.Emit(OpCodes.Pop);
			}
			else 
			{
				if (returnType.IsValueType) 
				{
					il.Emit(OpCodes.Unbox, returnType);
					// load boxed value from heap
					ILUtils.EmitTypedLdind(il, returnType);
				}
			}
			// load out/ref parameter values
			if (parameterTypes.Length > 0) 
			{
				for(int i = 0; i < parameterTypes.Length; i++)
				{
					Type parameterType = parameterTypes[i];
					if ( parameterType.IsByRef ) 
					{
						Type elementType = parameterType.GetElementType();
						// address of byref arg
						il.Emit(OpCodes.Ldarg_S, (sbyte) (i+1));
						// fetch value from args array
						il.Emit(OpCodes.Ldloc_0);
						il.Emit(OpCodes.Ldc_I4_S, (sbyte) i);
						il.Emit(OpCodes.Ldelem_Ref);
						// unbox if necessary
						if ( elementType.IsValueType )
						{
							il.Emit(OpCodes.Unbox, elementType);
							ILUtils.EmitTypedLdind(il, elementType);
						}
						// store indirectly into ref arg
						ILUtils.EmitTypedStind(il, elementType);
					}
				}
			}

			il.Emit(OpCodes.Ret);
		}
		/// <summary>
		/// Returns the array of parameters types given a
		/// <see cref="MethodInfo"/>
		/// </summary>
		/// <param name="mi">method we want the parameter types of</param>
		/// <returns>array of parameter types in method signature</returns>
		private static Type[] getParameterTypes(MethodInfo mi) 
		{
			ArrayList types = new ArrayList();
			foreach (ParameterInfo pi in mi.GetParameters()) 
			{
				types.Add(pi.ParameterType);
			}
			return (Type[]) types.ToArray(typeof(Type));
		}
	}	
}
