//
// ReflectionHelper.cs
//
// Author:
//   Jb Evain (jbevain@gmail.com)
//
// (C) 2005 Jb Evain
// (C) 2006 Evaluant RC S.A.
// (C) 2007 Jb Evain
//
// 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.
//

namespace Mono.Cecil {

	using System;
	using System.Collections;
	using SR = System.Reflection;
	using System.Text;

	internal sealed class ReflectionHelper {

		ModuleDefinition m_module;

		public ReflectionHelper (ModuleDefinition module)
		{
			m_module = module;
		}

		public AssemblyNameReference ImportAssembly (SR.Assembly asm)
		{
			AssemblyNameReference asmRef = GetAssemblyNameReference (asm.GetName ());
			if (asmRef != null)
				return asmRef;

			SR.AssemblyName asmName = asm.GetName ();
			asmRef = new AssemblyNameReference (
				asmName.Name, asmName.CultureInfo.Name, asmName.Version);
			asmRef.PublicKeyToken = asmName.GetPublicKeyToken ();
			asmRef.HashAlgorithm = (AssemblyHashAlgorithm) asmName.HashAlgorithm;
			asmRef.Culture = asmName.CultureInfo.ToString ();
			m_module.AssemblyReferences.Add (asmRef);
			return asmRef;
		}

		AssemblyNameReference GetAssemblyNameReference (SR.AssemblyName name)
		{
			foreach (AssemblyNameReference reference in m_module.AssemblyReferences)
				if (reference.FullName == name.FullName)
					return reference;

			return null;
		}

		public static string GetTypeSignature (Type t)
		{
			if (t.HasElementType) {
				if (t.IsPointer)
					return string.Concat (GetTypeSignature (t.GetElementType ()), "*");
				else if (t.IsArray) {
					int rank = t.GetArrayRank ();
					if (rank == 1)
						return string.Concat (GetTypeSignature (t.GetElementType ()), "[]");

					StringBuilder sb = new StringBuilder ();
					sb.Append ('[');
					for (int i = 1; i < rank; i++)
						sb.Append (',');
					sb.Append (']');

					return string.Concat (GetTypeSignature (t.GetElementType ()), sb.ToString ());
				} else if (t.IsByRef)
					return string.Concat(GetTypeSignature(t.GetElementType()), "&");
			}

			if (IsGenericTypeSpec (t)) {
				StringBuilder sb = new StringBuilder ();
				sb.Append (GetTypeSignature (GetGenericTypeDefinition (t)));
				sb.Append ("<");
				Type [] genArgs = GetGenericArguments (t);
				for (int i = 0; i < genArgs.Length; i++) {
					if (i > 0)
						sb.Append (",");
					sb.Append (GetTypeSignature (genArgs [i]));
				}
				sb.Append (">");
				return sb.ToString ();
			}

			if (IsGenericParameter (t))
				return t.Name;

			if (t.DeclaringType != null)
				return string.Concat (t.DeclaringType.FullName, "/", t.Name);

			if (t.Namespace == null || t.Namespace.Length == 0)
				return t.Name;

			return string.Concat (t.Namespace, ".", t.Name);
		}

		static bool GetProperty (object o, string prop)
		{
			SR.PropertyInfo pi = o.GetType ().GetProperty (prop);
			if (pi == null)
				return false;

			return (bool) pi.GetValue (o, null);
		}

		public static bool IsGenericType (Type t)
		{
			return GetProperty (t, "IsGenericType");
		}

		static bool IsGenericParameter (Type t)
		{
			return GetProperty (t, "IsGenericParameter");
		}

		static bool IsGenericTypeDefinition (Type t)
		{
			return GetProperty (t, "IsGenericTypeDefinition");
		}

		static bool IsGenericTypeSpec (Type t)
		{
			return IsGenericType (t) && !IsGenericTypeDefinition (t);
		}

		static Type GetGenericTypeDefinition (Type t)
		{
			return (Type) t.GetType ().GetMethod ("GetGenericTypeDefinition").Invoke (t, null);
		}

		static Type [] GetGenericArguments (Type t)
		{
			return (Type []) t.GetType ().GetMethod ("GetGenericArguments").Invoke (t, null);
		}

		GenericInstanceType GetGenericType (Type t, TypeReference element, ImportContext context)
		{
			GenericInstanceType git = new GenericInstanceType (element);
			foreach (Type genArg in GetGenericArguments (t))
				git.GenericArguments.Add (ImportSystemType (genArg, context));

			return git;
		}

		static bool GenericParameterOfMethod (Type t)
		{
			return t.GetType ().GetProperty ("DeclaringMethod").GetValue (t, null) != null;
		}

		static GenericParameter GetGenericParameter (Type t, ImportContext context)
		{
			int pos = (int) t.GetType ().GetProperty ("GenericParameterPosition").GetValue (t, null);
			IGenericParameterProvider provider;
			if (GenericParameterOfMethod (t))
				provider = context.GenericContext.Method;
			else
				provider = context.GenericContext.Type;

			if (provider == null)
				throw new InvalidOperationException ("Invalid context");

			return provider.GenericParameters [pos];
		}

		TypeReference GetTypeSpec (Type t, ImportContext context)
		{
			Stack s = new Stack ();
			while (t.HasElementType || IsGenericTypeSpec (t)) {
				s.Push (t);
				if (t.HasElementType)
					t = t.GetElementType ();
				else if (IsGenericTypeSpec (t)) {
					t = (Type) t.GetType ().GetMethod ("GetGenericTypeDefinition").Invoke (t, null);
					break;
				}
			}

			TypeReference elementType = ImportSystemType (t, context);
			while (s.Count > 0) {
				t = (Type) s.Pop ();
				if (t.IsPointer)
					elementType = new PointerType (elementType);
				else if (t.IsArray)
					elementType = new ArrayType (elementType, t.GetArrayRank ());
				else if (t.IsByRef)
					elementType = new ReferenceType (elementType);
				else if (IsGenericTypeSpec (t))
					elementType = GetGenericType (t, elementType, context);
				else
					throw new ReflectionException ("Unknown element type");
			}

			return elementType;
		}

		TypeReference AdjustReference (Type type, TypeReference reference)
		{
			if (type.IsValueType && !reference.IsValueType)
				reference.IsValueType = true;

			if (IsGenericTypeDefinition (type)) {
				Type [] parameters = GetGenericArguments (type);
				for (int i = reference.GenericParameters.Count; i < parameters.Length; i++)
					reference.GenericParameters.Add (new GenericParameter (i, reference));
			}

			return reference;
		}

		public TypeReference ImportSystemType (Type t, ImportContext context)
		{
			if (t.HasElementType || IsGenericTypeSpec (t))
				return GetTypeSpec (t, context);

			if (IsGenericParameter (t))
				return GetGenericParameter (t, context);

			TypeReference type = m_module.TypeReferences [GetTypeSignature (t)];
			if (type != null)
				return AdjustReference (t, type);

			AssemblyNameReference asm = ImportAssembly (t.Assembly);
			if (t.DeclaringType != null) {
				type = new TypeReference (t.Name, string.Empty, asm, t.IsValueType);
				type.DeclaringType = ImportSystemType (t.DeclaringType, context);
			} else
				type = new TypeReference (t.Name, t.Namespace, asm, t.IsValueType);

			if (IsGenericTypeDefinition (t))
				foreach (Type genParam in GetGenericArguments (t))
					type.GenericParameters.Add (new GenericParameter (genParam.Name, type));

			context.GenericContext.Type = type;

			m_module.TypeReferences.Add (type);
			return type;
		}

		static string GetMethodBaseSignature (SR.MethodBase meth, Type declaringType, Type retType)
		{
			StringBuilder sb = new StringBuilder ();
			sb.Append (GetTypeSignature (retType));
			sb.Append (' ');
			sb.Append (GetTypeSignature (declaringType));
			sb.Append ("::");
			sb.Append (meth.Name);
			if (IsGenericMethodSpec (meth)) {
				sb.Append ("<");
				Type [] genArgs = GetGenericArguments (meth as SR.MethodInfo);
				for (int i = 0; i < genArgs.Length; i++) {
					if (i > 0)
						sb.Append (",");
					sb.Append (GetTypeSignature (genArgs [i]));
				}
				sb.Append (">");
			}
			sb.Append ("(");
			SR.ParameterInfo [] parameters = meth.GetParameters ();
			for (int i = 0; i < parameters.Length; i++) {
				if (i > 0)
					sb.Append (",");
				sb.Append (GetTypeSignature (parameters [i].ParameterType));
			}
			sb.Append (")");
			return sb.ToString ();
		}

		static bool IsGenericMethod (SR.MethodBase mb)
		{
			return GetProperty (mb, "IsGenericMethod");
		}

		static bool IsGenericMethodDefinition (SR.MethodBase mb)
		{
			return GetProperty (mb, "IsGenericMethodDefinition");
		}

		static bool IsGenericMethodSpec (SR.MethodBase mb)
		{
			return IsGenericMethod (mb) && !IsGenericMethodDefinition (mb);
		}

		static Type [] GetGenericArguments (SR.MethodInfo mi)
		{
			return (Type []) mi.GetType ().GetMethod ("GetGenericArguments").Invoke (mi, null);
		}

		static int GetMetadataToken (SR.MethodInfo mi)
		{
			return (int) mi.GetType ().GetProperty ("MetadataToken").GetValue (mi, null);
		}

		MethodReference ImportGenericInstanceMethod (SR.MethodInfo mi, ImportContext context)
		{
			SR.MethodInfo gmd = (SR.MethodInfo) mi.GetType ().GetMethod ("GetGenericMethodDefinition").Invoke (mi, null);
			GenericInstanceMethod gim = new GenericInstanceMethod (
				ImportMethodBase (gmd, gmd.ReturnType, context));

			foreach (Type genArg in GetGenericArguments (mi))
				gim.GenericArguments.Add (ImportSystemType (genArg, context));

			return gim;
		}

		MethodReference ImportMethodBase (SR.MethodBase mb, Type retType, ImportContext context)
		{
			if (IsGenericMethod (mb) && !IsGenericMethodDefinition (mb))
				return ImportGenericInstanceMethod ((SR.MethodInfo) mb, context);

			Type originalDecType = mb.DeclaringType;
			Type declaringTypeDef = originalDecType;
			while (IsGenericTypeSpec (declaringTypeDef))
				declaringTypeDef = GetGenericTypeDefinition (declaringTypeDef);

			if (mb.DeclaringType != declaringTypeDef && mb is SR.MethodInfo) {
				int mt = GetMetadataToken (mb as SR.MethodInfo);
				// hack to get the generic method definition from the constructed method
				foreach (SR.MethodInfo mi in declaringTypeDef.GetMethods ()) {
					if (GetMetadataToken (mi) == mt) {
						mb = mi;
						retType = mi.ReturnType;
						break;
					}
				}
			}

			string sig = GetMethodBaseSignature (mb, originalDecType, retType);
			MethodReference meth = (MethodReference) GetMemberReference (sig);
			if (meth != null)
				return meth;

			meth = new MethodReference (
				mb.Name,
				(mb.CallingConvention & SR.CallingConventions.HasThis) > 0,
				(mb.CallingConvention & SR.CallingConventions.ExplicitThis) > 0,
				MethodCallingConvention.Default); // TODO: get the real callconv
			meth.DeclaringType = ImportSystemType (originalDecType, context);

			if (IsGenericMethod (mb))
				foreach (Type genParam in GetGenericArguments (mb as SR.MethodInfo))
					meth.GenericParameters.Add (new GenericParameter (genParam.Name, meth));

			TypeReference contextType = context.GenericContext.Type;
			MethodReference contextMethod = context.GenericContext.Method;

			context.GenericContext.Method = meth;
			context.GenericContext.Type = ImportSystemType (declaringTypeDef, context);

			meth.ReturnType.ReturnType = ImportSystemType (retType, context);

			SR.ParameterInfo [] parameters = mb.GetParameters ();
			for (int i = 0; i < parameters.Length; i++)
				meth.Parameters.Add (new ParameterDefinition (
					ImportSystemType (parameters [i].ParameterType, context)));

			context.GenericContext.Type = contextType;
			context.GenericContext.Method = contextMethod;

			m_module.MemberReferences.Add (meth);
			return meth;
		}

		public MethodReference ImportConstructorInfo (SR.ConstructorInfo ci, ImportContext context)
		{
			return ImportMethodBase (ci, typeof (void), context);
		}

		public MethodReference ImportMethodInfo (SR.MethodInfo mi, ImportContext context)
		{
			return ImportMethodBase (mi, mi.ReturnType, context);
		}

		static string GetFieldSignature (SR.FieldInfo field)
		{
			StringBuilder sb = new StringBuilder ();
			sb.Append (GetTypeSignature (field.FieldType));
			sb.Append (' ');
			sb.Append (GetTypeSignature (field.DeclaringType));
			sb.Append ("::");
			sb.Append (field.Name);
			return sb.ToString ();
		}

		public FieldReference ImportFieldInfo (SR.FieldInfo fi, ImportContext context)
		{
			string sig = GetFieldSignature (fi);
			FieldReference f = (FieldReference) GetMemberReference (sig);
			if (f != null)
				return f;

			f = new FieldReference (
				fi.Name,
				ImportSystemType (fi.DeclaringType, context),
				ImportSystemType (fi.FieldType, context));

			m_module.MemberReferences.Add (f);
			return f;
		}

		MemberReference GetMemberReference (string signature)
		{
			foreach (MemberReference reference in m_module.MemberReferences)
				if (reference.ToString () == signature)
					return reference;

			return null;
		}
	}
}
