//
// ContractRuntime.cs
//
// Authors:
//	Chris Bacon (chrisbacon76@gmail.com)
//
// Copyright (C) 2010 Chris Bacon
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Mono.Cecil;
using System.Diagnostics.Contracts;
using Mono.Cecil.Cil;
using System.Diagnostics;
using System.Runtime.ConstrainedExecution;
using System.Runtime.CompilerServices;

namespace Mono.CodeContracts.Rewrite {
	class ContractsRuntime {

		private const string Namespace = "System.Diagnostics.Contracts";

		public ContractsRuntime (ModuleDefinition module, RewriterOptions options)
		{
			this.module = module;
			this.options = options;
		}

		private ModuleDefinition module;
		private RewriterOptions options;

		private TypeDefinition typeContractsRuntime = null;
		private TypeDefinition typeContractException = null;
		private MethodDefinition methodContractExceptionCons = null;
		private MethodDefinition methodTriggerFailure = null;
		private MethodDefinition methodReportFailure = null;
		private MethodDefinition methodRequires = null;

		private void EnsureTypeContractRuntime ()
		{
			if (this.typeContractsRuntime == null) {
				// namespace System.Diagnostics.Contracts {
				//     [CompilerGenerated]
				//     private static class __ContractsRuntime {
				//     }
				// }
				
				// Create type
				TypeReference typeObject = this.module.Import (typeof (object));
				TypeDefinition type = new TypeDefinition ("__ContractsRuntime", Namespace,
					TypeAttributes.Abstract | TypeAttributes.Sealed | TypeAttributes.NotPublic | TypeAttributes.AnsiClass | TypeAttributes.AutoClass, // | TypeAttributes.BeforeFieldInit,
					typeObject);
				this.module.Types.Add (type);
				// Attach custom attributes
				var attrCompilerGeneratedCons = typeof (CompilerGeneratedAttribute).GetConstructor (Type.EmptyTypes);
				CustomAttribute attrCompilerGenerated = new CustomAttribute (this.module.Import (attrCompilerGeneratedCons));
				type.CustomAttributes.Add (attrCompilerGenerated);
				// Store type
				this.typeContractsRuntime = type;
			}
		}

		private void EnsureTypeContractException ()
		{
			if (this.options.ThrowOnFailure && this.typeContractException == null) {
				// [CompilerGenerated]
				// private class ContractException : Exception {
				//     internal ContractException(ContractFailureKind kind, string usermsg, string condition, Exception inner)
				//         : base(failure, inner)
				//     {
				//     }
				// }
				
				// Prepare type references
				TypeReference typeVoid = this.module.Import (typeof (void));
				TypeReference typeContractFailureKind = this.module.Import (typeof (ContractFailureKind));
				TypeReference typeString = this.module.Import (typeof (string));
				TypeReference typeException = this.module.Import (typeof (Exception));
				// Create type
				TypeDefinition type = new TypeDefinition ("ContractException", Namespace,
					TypeAttributes.NestedPrivate | TypeAttributes.AnsiClass | TypeAttributes.AutoClass, typeException);
				//this.typeContractsRuntime.NestedTypes.Add (type);
				this.module.Types.Add(type);
				// Create constructor
				MethodDefinition cons = new MethodDefinition (".ctor",
					MethodAttributes.Assembly | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, typeVoid);
				cons.Parameters.Add (new ParameterDefinition ("kind", ParameterAttributes.None, typeContractFailureKind));
				cons.Parameters.Add (new ParameterDefinition ("failure", ParameterAttributes.None, typeString));
				cons.Parameters.Add (new ParameterDefinition ("usermsg", ParameterAttributes.None, typeString));
				cons.Parameters.Add (new ParameterDefinition ("condition", ParameterAttributes.None, typeString));
				cons.Parameters.Add (new ParameterDefinition ("inner", ParameterAttributes.None, typeException));
				var il = cons.Body.GetILProcessor ();
				il.Emit (OpCodes.Ldarg_0);
				il.Emit (OpCodes.Ldarg_2);
				il.Emit (OpCodes.Ldarg_S, cons.Parameters [4]);
				MethodReference mExceptionCons = this.module.Import (typeof (Exception).GetConstructor (new [] { typeof (string), typeof (Exception) }));
				il.Emit (OpCodes.Call, mExceptionCons);
				il.Emit (OpCodes.Ret);
				type.Methods.Add (cons);
				// Attach custom attributes
				var attrCompilerGeneratedCons = typeof (CompilerGeneratedAttribute).GetConstructor (Type.EmptyTypes);
				CustomAttribute attrCompilerGenerated = new CustomAttribute (this.module.Import (attrCompilerGeneratedCons));
				type.CustomAttributes.Add (attrCompilerGenerated);
				// Store constructor and type
				this.methodContractExceptionCons = cons;
				this.typeContractException = type;
			}
		}

		private void EnsureMethodTriggerFailure ()
		{
			if (this.methodTriggerFailure == null) {
				// if the ThrowOnFailure option is true, then:
				// internal static void TriggerFailure(ContractFailureKind kind, string message, string userMessage, string conditionText, Exception inner)
				// {
				//     throw new ContractException(kind, message, userMessage, conditionText, inner);
				// }
				
				// if the ThrowOnFailure option is false, then:
				// internal static void TriggerFailure(ContractFailureKind kind, string message, string userMessage, string conditionText, Exception inner)
				// {
				//     Debug.Fail(message, userMessage);
				// }
				
				// Prepare type references
				TypeReference typeVoid = this.module.Import (typeof (void));
				TypeReference typeContractFailureKind = this.module.Import (typeof (ContractFailureKind));
				TypeReference typeString = this.module.Import (typeof (string));
				TypeReference typeException = this.module.Import (typeof (Exception));
				// Create method
				MethodDefinition method = new MethodDefinition ("TriggerFailure",
					MethodAttributes.Assembly | MethodAttributes.Static, typeVoid);
				method.Parameters.Add (new ParameterDefinition ("kind", ParameterAttributes.None, typeContractFailureKind));
				method.Parameters.Add (new ParameterDefinition ("message", ParameterAttributes.None, typeString));
				method.Parameters.Add (new ParameterDefinition ("userMessage", ParameterAttributes.None, typeString));
				method.Parameters.Add (new ParameterDefinition ("conditionText", ParameterAttributes.None, typeString));
				method.Parameters.Add (new ParameterDefinition ("inner", ParameterAttributes.None, typeException));
				var il = method.Body.GetILProcessor ();
				if (this.options.ThrowOnFailure) {
					il.Emit (OpCodes.Ldarg_0);
					il.Emit (OpCodes.Ldarg_1);
					il.Emit (OpCodes.Ldarg_2);
					il.Emit (OpCodes.Ldarg_3);
					il.Emit (OpCodes.Ldarg_S, method.Parameters [4]);
					il.Emit (OpCodes.Newobj, this.methodContractExceptionCons);
					il.Emit (OpCodes.Throw);
				} else {
					var mDebugFail = typeof (Debug).GetMethod ("Fail", new [] { typeof (string), typeof(string) });
					MethodReference methodDebugFail = this.module.Import (mDebugFail);
					il.Emit (OpCodes.Ldarg_1);
					il.Emit (OpCodes.Ldarg_2);
					il.Emit (OpCodes.Call, methodDebugFail);
					il.Emit (OpCodes.Ret);
				}
				this.typeContractsRuntime.Methods.Add (method);
				this.methodTriggerFailure = method;
			}
		}

		private void EnsureMethodReportFailure ()
		{
			if (this.methodReportFailure == null) {
				// internal static void ReportFailure(ContractFailureKind kind, string message, string conditionText, Exception inner)
				// {
				//     string s = ContractHelper.RaiseContractFailedEvent(kind, message, conditionText, inner);
				//     if (s != null) {
				//         TriggerFailure(kind, s, message, conditionText, inner);
				//     }
				// }
				
				// Prepare type references
				TypeReference typeVoid = this.module.Import (typeof (void));
				TypeReference typeContractFailureKind = this.module.Import (typeof (ContractFailureKind));
				TypeReference typeString = this.module.Import (typeof (string));
				TypeReference typeException = this.module.Import (typeof (Exception));
				var helper = typeof (ContractHelper);
				MethodReference mRaiseContractFailedEvent = this.module.Import (helper.GetMethod ("RaiseContractFailedEvent"));
				// Create method
				MethodDefinition method = new MethodDefinition ("ReportFailure",
					MethodAttributes.Assembly | MethodAttributes.Static, typeVoid);
				method.Parameters.Add (new ParameterDefinition ("kind", ParameterAttributes.None, typeContractFailureKind));
				method.Parameters.Add (new ParameterDefinition ("message", ParameterAttributes.None, typeString));
				method.Parameters.Add (new ParameterDefinition ("conditionText", ParameterAttributes.None, typeString));
				method.Parameters.Add (new ParameterDefinition ("inner", ParameterAttributes.None, typeException));
				VariableDefinition vMsg = new VariableDefinition (typeString);
				method.Body.Variables.Add (vMsg);
				method.Body.InitLocals = true;
				var il = method.Body.GetILProcessor ();
				il.Emit (OpCodes.Ldarg_0);
				il.Emit (OpCodes.Ldarg_1);
				il.Emit (OpCodes.Ldarg_2);
				il.Emit (OpCodes.Ldarg_3);
				il.Emit (OpCodes.Call, mRaiseContractFailedEvent);
				il.Emit (OpCodes.Stloc_0);
				il.Emit (OpCodes.Ldloc_0);
				var instRet = il.Create (OpCodes.Ret);
				il.Emit (OpCodes.Brfalse_S, instRet);
				il.Emit (OpCodes.Ldarg_0);
				il.Emit (OpCodes.Ldloc_0);
				il.Emit (OpCodes.Ldarg_1);
				il.Emit (OpCodes.Ldarg_2);
				il.Emit (OpCodes.Ldarg_3);
				il.Emit (OpCodes.Call, this.methodTriggerFailure);
				il.Append (instRet);
				this.typeContractsRuntime.Methods.Add (method);
				this.methodReportFailure = method;
			}
		}

		private void EnsureGlobal ()
		{
			this.EnsureTypeContractRuntime ();
			this.EnsureTypeContractException ();
			this.EnsureMethodTriggerFailure ();
			this.EnsureMethodReportFailure ();
		}

		public MethodDefinition GetRequires ()
		{
			this.EnsureGlobal ();
			if (this.methodRequires == null) {
				// [DebuggerNonUserCode]
				// [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
				// internal static void Requires(bool condition, string message, string conditionText)
				// {
				//     if (!condition) {
				//         ReportFailure(ContractFailureKind.Precondition, message, conditionText, null);
				//     }
				// }
				
				// Prepare type references
				TypeReference typeVoid = this.module.Import (typeof (void));
				TypeReference typeBoolean = this.module.Import (typeof (bool));
				TypeReference typeString = this.module.Import (typeof (string));
				// Create method
				MethodDefinition method = new MethodDefinition ("Requires",
				    MethodAttributes.Assembly | MethodAttributes.Static, typeVoid);
				method.Parameters.Add (new ParameterDefinition ("condition", ParameterAttributes.None, typeBoolean));
				method.Parameters.Add (new ParameterDefinition ("message", ParameterAttributes.None, typeString));
				method.Parameters.Add (new ParameterDefinition ("conditionText", ParameterAttributes.None, typeString));
				var il = method.Body.GetILProcessor ();
				il.Emit (OpCodes.Ldarg_0);
				var instRet = il.Create(OpCodes.Ret);
				il.Emit (OpCodes.Brtrue_S, instRet);
				il.Emit (OpCodes.Ldc_I4_0); // Assumes ContractFailureKind.Precondition == 0
				il.Emit (OpCodes.Ldarg_1);
				il.Emit (OpCodes.Ldarg_2);
				il.Emit (OpCodes.Ldnull);
				il.Emit (OpCodes.Call, this.methodReportFailure);
				il.Append (instRet);
				this.typeContractsRuntime.Methods.Add (method);
				// Attach custom attributes
				var attrDebugNonUserCodeCons = typeof (DebuggerNonUserCodeAttribute).GetConstructor (Type.EmptyTypes);
				CustomAttribute attrDebugNonUserCode = new CustomAttribute (this.module.Import (attrDebugNonUserCodeCons));
				method.CustomAttributes.Add (attrDebugNonUserCode);
				var attrReliabilityContractCons = typeof (ReliabilityContractAttribute).GetConstructor (new [] { typeof (Consistency), typeof (Cer) });
				// Blob for attribute: new ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)
				byte [] blob = new byte [] { 1, 0, 3, 0, 0, 0, 1, 0, 0, 0 };
				CustomAttribute attrReliabilityContract = new CustomAttribute (this.module.Import (attrReliabilityContractCons), blob);
				method.CustomAttributes.Add (attrReliabilityContract);
				// Store method
				this.methodRequires = method;
			}
			return this.methodRequires;
		}

	}
}
