using System;
using System.Reflection;
using System.Reflection.Emit;
using System.Security;
using System.Security.Permissions;
using NHibernate.Properties;
using NHibernate.Util;

namespace NHibernate.Bytecode.Lightweight
{
	public class ReflectionOptimizer : IReflectionOptimizer, IInstantiationOptimizer
	{
		private readonly IAccessOptimizer accessOptimizer;
		private readonly CreateInstanceInvoker createInstanceMethod;
		protected readonly System.Type mappedType;
		private readonly System.Type typeOfThis;

		public IAccessOptimizer AccessOptimizer
		{
			get { return accessOptimizer; }
		}

		public IInstantiationOptimizer InstantiationOptimizer
		{
			get { return this; }
		}

		public virtual object CreateInstance()
		{
			return createInstanceMethod != null ? createInstanceMethod() : null;
		}

		/// <summary>
		/// Class constructor.
		/// </summary>
		[Obsolete("This constructor has no usages and will be removed in a future version")]
		public ReflectionOptimizer(System.Type mappedType, IGetter[] getters, ISetter[] setters)
			: this(mappedType, getters, setters, null, null)
		{
		}

		/// <summary>
		/// Class constructor.
		/// </summary>
		public ReflectionOptimizer(
			System.Type mappedType, IGetter[] getters, ISetter[] setters,
			IGetter specializedGetter, ISetter specializedSetter)
		{
			// save off references
			this.mappedType = mappedType;
			typeOfThis = mappedType.IsValueType ? mappedType.MakeByRefType() : mappedType;
			//this.getters = getters;
			//this.setters = setters;

			GetPropertyValuesInvoker getInvoker = GenerateGetPropertyValuesMethod(getters);
			SetPropertyValuesInvoker setInvoker = GenerateSetPropertyValuesMethod(setters);

			var getMethods = new GetPropertyValueInvoker[getters.Length];
			for (var i = 0; i < getters.Length; i++)
			{
				getMethods[i] = GenerateGetPropertyValueMethod(getters[i]);
			}

			var setMethods = new SetPropertyValueInvoker[setters.Length];
			for (var i = 0; i < setters.Length; i++)
			{
				setMethods[i] = GenerateSetPropertyValueMethod(setters[i]);
			}

			accessOptimizer = new AccessOptimizer(
				getInvoker,
				setInvoker,
				getMethods,
				setMethods,
				GenerateGetPropertyValueMethod(specializedGetter),
				GenerateSetPropertyValueMethod(specializedSetter)
			);

			createInstanceMethod = CreateCreateInstanceMethod(mappedType);
		}

		/// <summary>
		/// Generates a dynamic method which creates a new instance of <paramref name="type" />
		/// when invoked.
		/// </summary>
		protected virtual CreateInstanceInvoker CreateCreateInstanceMethod(System.Type type)
		{
			if (type.IsInterface || type.IsAbstract)
			{
				return null;
			}

			var method = new DynamicMethod(string.Empty, typeof (object), null, type, true);

			ILGenerator il = method.GetILGenerator();

			if (type.IsValueType)
			{
				LocalBuilder tmpLocal = il.DeclareLocal(type);
				il.Emit(OpCodes.Ldloca, tmpLocal);
				il.Emit(OpCodes.Initobj, type);
				il.Emit(OpCodes.Ldloc, tmpLocal);
				il.Emit(OpCodes.Box, type);
				il.Emit(OpCodes.Ret);

				return (CreateInstanceInvoker)method.CreateDelegate(typeof(CreateInstanceInvoker));
			}
			else
			{
				ConstructorInfo constructor = ReflectHelper.GetDefaultConstructor(type);
				if (constructor != null)
				{
					il.Emit(OpCodes.Newobj, constructor);
					il.Emit(OpCodes.Ret);

					return (CreateInstanceInvoker) method.CreateDelegate(typeof (CreateInstanceInvoker));
				}
				else
				{
					ThrowExceptionForNoDefaultCtor(type);
				}
			}
			return null;
		}

		protected virtual void ThrowExceptionForNoDefaultCtor(System.Type type)
		{
			throw new InstantiationException("Object class " + type + " must declare a default (no-argument) constructor", type);
		}

		protected DynamicMethod CreateDynamicMethod(System.Type returnType, System.Type[] argumentTypes)
		{
			var owner = mappedType.IsInterface ? typeof (object) : mappedType;
			var canSkipChecks = CanSkipVisibilityChecks();
			return new DynamicMethod(string.Empty, returnType, argumentTypes, owner, canSkipChecks);
		}

		private static bool CanSkipVisibilityChecks()
		{
#if NETFX
			var permissionSet = new PermissionSet(PermissionState.None);
			permissionSet.AddPermission(new ReflectionPermission(ReflectionPermissionFlag.MemberAccess));
			return permissionSet.IsSubsetOf(AppDomain.CurrentDomain.PermissionSet);
#else
			return false;
#endif
		}

		private static void EmitCastToReference(ILGenerator il, System.Type type)
		{
			if (type.IsValueType)
			{
				il.Emit(OpCodes.Unbox, type);
			}
			else
			{
				il.Emit(OpCodes.Castclass, type);
			}
		}

		private static readonly MethodInfo GetterCallbackInvoke = ReflectHelper.GetMethod<GetterCallback>(
			g => g.Invoke(null, 0));

		private GetPropertyValueInvoker GenerateGetPropertyValueMethod(IGetter getter)
		{
			if (getter == null)
				return null;
			if (!(getter is IOptimizableGetter optimizableGetter))
				return getter.Get;

			var method = CreateDynamicMethod(typeof(object), new[] { typeof(object) });
			var il = method.GetILGenerator();

			// object (target) { (object) ((ClassType) target).GetMethod(); }
			il.Emit(OpCodes.Ldarg_0);
			EmitCastToReference(il, mappedType);
			optimizableGetter.Emit(il);
			EmitUtil.EmitBoxIfNeeded(il, getter.ReturnType);
			il.Emit(OpCodes.Ret);

			return (GetPropertyValueInvoker) method.CreateDelegate(typeof(GetPropertyValueInvoker));
		}

		private SetPropertyValueInvoker GenerateSetPropertyValueMethod(ISetter setter)
		{
			if (setter == null)
				return null;
			if (!(setter is IOptimizableSetter optimizableSetter))
				return setter.Set;

			// void (target, value) { ((ClassType) target).SetMethod((FieldType) value); }
			var method = CreateDynamicMethod(null, new[] { typeof(object), typeof(object) });
			var il = method.GetILGenerator();
			il.Emit(OpCodes.Ldarg_0);
			EmitCastToReference(il, mappedType);
			il.Emit(OpCodes.Ldarg_1);
			EmitUtil.PreparePropertyForSet(il, optimizableSetter.Type);
			optimizableSetter.Emit(il);
			il.Emit(OpCodes.Ret);

			return (SetPropertyValueInvoker) method.CreateDelegate(typeof(SetPropertyValueInvoker));
		}

		/// <summary>
		/// Generates a dynamic method on the given type.
		/// </summary>
		private GetPropertyValuesInvoker GenerateGetPropertyValuesMethod(IGetter[] getters)
		{
			var methodArguments = new[] {typeof (object), typeof (GetterCallback)};
			DynamicMethod method = CreateDynamicMethod(typeof (object[]), methodArguments);

			ILGenerator il = method.GetILGenerator();

			LocalBuilder thisLocal = il.DeclareLocal(typeOfThis);
			LocalBuilder dataLocal = il.DeclareLocal(typeof (object[]));

			// Cast the 'this' pointer to the appropriate type and store it in a local variable
			il.Emit(OpCodes.Ldarg_0);
			EmitCastToReference(il, mappedType);
			il.Emit(OpCodes.Stloc, thisLocal);

			// Allocate the values array and store it in a local variable
			il.Emit(OpCodes.Ldc_I4, getters.Length);
			il.Emit(OpCodes.Newarr, typeof (object));
			il.Emit(OpCodes.Stloc, dataLocal);

			//get all the data from the object into the data array to be returned
			for (int i = 0; i < getters.Length; i++)
			{
				// get the member accessors
				IGetter getter = getters[i];

				// queue up the array storage location for the value
				il.Emit(OpCodes.Ldloc, dataLocal);
				il.Emit(OpCodes.Ldc_I4, i);

				// get the value...
				var optimizableGetter = getter as IOptimizableGetter;
				if (optimizableGetter != null)
				{
					// using the getter's emitted IL code
					il.Emit(OpCodes.Ldloc, thisLocal);
					optimizableGetter.Emit(il);
					EmitUtil.EmitBoxIfNeeded(il, getter.ReturnType);
				}
				else
				{
					// using the getter itself via a callback
					MethodInfo invokeMethod = GetterCallbackInvoke;
					il.Emit(OpCodes.Ldarg_1);
					il.Emit(OpCodes.Ldarg_0);
					il.Emit(OpCodes.Ldc_I4, i);
					il.Emit(OpCodes.Callvirt, invokeMethod);
				}

				//store the value
				il.Emit(OpCodes.Stelem_Ref);
			}

			// Return the data array
			il.Emit(OpCodes.Ldloc, dataLocal.LocalIndex);
			il.Emit(OpCodes.Ret);

			return (GetPropertyValuesInvoker) method.CreateDelegate(typeof (GetPropertyValuesInvoker));
		}

		private static readonly MethodInfo SetterCallbackInvoke = ReflectHelper.GetMethod<SetterCallback>(
			g => g.Invoke(null, 0, null));

		/// <summary>
		/// Generates a dynamic method on the given type.
		/// </summary>
		/// <returns></returns>
		private SetPropertyValuesInvoker GenerateSetPropertyValuesMethod(ISetter[] setters)
		{
			var methodArguments = new[] {typeof (object), typeof (object[]), typeof (SetterCallback)};
			DynamicMethod method = CreateDynamicMethod(null, methodArguments);

			ILGenerator il = method.GetILGenerator();

			// Declare a local variable used to store the object reference (typed)
			LocalBuilder thisLocal = il.DeclareLocal(typeOfThis);
			il.Emit(OpCodes.Ldarg_0);
			EmitCastToReference(il, mappedType);
			il.Emit(OpCodes.Stloc, thisLocal.LocalIndex);

			for (int i = 0; i < setters.Length; i++)
			{
				// get the member accessor
				ISetter setter = setters[i];

				var optimizableSetter = setter as IOptimizableSetter;

				if (optimizableSetter != null)
				{
					// load 'this'
					il.Emit(OpCodes.Ldloc, thisLocal);

					// load the value from the data array
					il.Emit(OpCodes.Ldarg_1);
					il.Emit(OpCodes.Ldc_I4, i);
					il.Emit(OpCodes.Ldelem_Ref);

					EmitUtil.PreparePropertyForSet(il, optimizableSetter.Type);

					// using the setter's emitted IL
					optimizableSetter.Emit(il);
				}
				else
				{
					// using the setter itself via a callback
					MethodInfo invokeMethod = SetterCallbackInvoke;
					il.Emit(OpCodes.Ldarg_2);
					il.Emit(OpCodes.Ldarg_0);
					il.Emit(OpCodes.Ldc_I4, i);

					// load the value from the data array
					il.Emit(OpCodes.Ldarg_1);
					il.Emit(OpCodes.Ldc_I4, i);
					il.Emit(OpCodes.Ldelem_Ref);

					il.Emit(OpCodes.Callvirt, invokeMethod);
				}
			}

			// Setup the return
			il.Emit(OpCodes.Ret);

			return (SetPropertyValuesInvoker) method.CreateDelegate(typeof (SetPropertyValuesInvoker));
		}
	}
}