//
// DefaultReferenceImporter.cs
//
// Author:
//   Jb Evain (jbevain@novell.com)
//
// (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;

	internal class DefaultImporter : IImporter {

		ModuleDefinition m_module;

		public ModuleDefinition Module {
			get { return m_module; }
		}

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

		public AssemblyNameReference ImportAssembly (AssemblyNameReference asm)
		{
			AssemblyNameReference asmRef = GetAssemblyNameReference (asm);
			if (asmRef != null)
				return asmRef;

			asmRef = new AssemblyNameReference (
				asm.Name, asm.Culture, asm.Version);
			asmRef.PublicKeyToken = asm.PublicKeyToken;
			asmRef.HashAlgorithm = asm.HashAlgorithm;
			m_module.AssemblyReferences.Add (asmRef);
			return asmRef;
		}

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

			return null;
		}

		TypeSpecification GetTypeSpec (TypeSpecification original, ImportContext context)
		{
			TypeSpecification typeSpec;

			TypeReference elementType = ImportTypeReference (original.ElementType, context);
			if (original is PointerType) {
				typeSpec = new PointerType (elementType);
			} else if (original is ArrayType) { // deal with complex arrays
				typeSpec = new ArrayType (elementType);
			} else if (original is ReferenceType) {
				typeSpec = new ReferenceType (elementType);
			} else if (original is GenericInstanceType) {
				GenericInstanceType git = original as GenericInstanceType;
				GenericInstanceType genElemType = new GenericInstanceType (elementType);

				context.GenericContext.CheckProvider (genElemType.GetOriginalType (), git.GenericArguments.Count);
				foreach (TypeReference arg in git.GenericArguments)
					genElemType.GenericArguments.Add (ImportTypeReference (arg, context));

				typeSpec = genElemType;
			} else if (original is ModifierOptional) {
				TypeReference mt = (original as ModifierOptional).ModifierType;
				typeSpec = new ModifierOptional (elementType, ImportTypeReference (mt, context));
			} else if (original is ModifierRequired) {
				TypeReference mt = (original as ModifierRequired).ModifierType;
				typeSpec = new ModifierRequired (elementType, ImportTypeReference (mt, context));
			} else if (original is SentinelType) {
				typeSpec = new SentinelType (elementType);
			} else if (original is FunctionPointerType) {
				FunctionPointerType ori = original as FunctionPointerType;

				FunctionPointerType fnptr = new FunctionPointerType (
					ori.HasThis,
					ori.ExplicitThis,
					ori.CallingConvention,
					new MethodReturnType (ImportTypeReference (ori.ReturnType.ReturnType, context)));

				foreach (ParameterDefinition parameter in ori.Parameters)
					fnptr.Parameters.Add (new ParameterDefinition (ImportTypeReference (parameter.ParameterType, context)));

				typeSpec = fnptr;
			} else
				throw new ReflectionException ("Unknown element type: {0}", original.GetType ().Name);

			return typeSpec;
		}

		static GenericParameter GetGenericParameter (GenericParameter gp, ImportContext context)
		{
			GenericParameter p;
			if (gp.Owner is TypeReference)
				p = context.GenericContext.Type.GenericParameters [gp.Position];
			else if (gp.Owner is MethodReference)
				p = context.GenericContext.Method.GenericParameters [gp.Position];
			else
				throw new NotSupportedException ();

			return p;
		}

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

			if (type.HasGenericParameters) {
				for (int i = reference.GenericParameters.Count; i < type.GenericParameters.Count; i++)
					reference.GenericParameters.Add (new GenericParameter (i, reference));
			}

			return reference;
		}

		public virtual TypeReference ImportTypeReference (TypeReference t, ImportContext context)
		{
			if (t.Module == m_module)
				return t;

			if (t is TypeSpecification)
				return GetTypeSpec (t as TypeSpecification, context);

			if (t is GenericParameter)
				return GetGenericParameter (t as GenericParameter, context);

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

			AssemblyNameReference asm;
			if (t.Scope is AssemblyNameReference)
				asm = ImportAssembly ((AssemblyNameReference) t.Scope);
			else if (t.Scope is ModuleDefinition)
				asm = ImportAssembly (((ModuleDefinition) t.Scope).Assembly.Name);
			else
				throw new NotImplementedException ();

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

			TypeReference contextType = context.GenericContext.Type;

			context.GenericContext.Type = type;

			GenericParameter.CloneInto (t, type, context);

			context.GenericContext.Type = contextType;

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

		public virtual FieldReference ImportFieldReference (FieldReference fr, ImportContext context)
		{
			if (fr.DeclaringType.Module == m_module)
				return fr;

			FieldReference field = (FieldReference) GetMemberReference (fr);
			if (field != null)
				return field;

			field = new FieldReference (
				fr.Name,
				ImportTypeReference (fr.DeclaringType, context),
				ImportTypeReference (fr.FieldType, context));

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

		MethodReference GetMethodSpec (MethodReference meth, ImportContext context)
		{
			if (!(meth is GenericInstanceMethod))
				return null;

			GenericInstanceMethod gim = meth as GenericInstanceMethod;
			GenericInstanceMethod ngim = new GenericInstanceMethod (
				ImportMethodReference (gim.ElementMethod, context));

			context.GenericContext.CheckProvider (ngim.GetOriginalMethod (), gim.GenericArguments.Count);
			foreach (TypeReference arg in gim.GenericArguments)
				ngim.GenericArguments.Add (ImportTypeReference (arg, context));

			return ngim;
		}

		public virtual MethodReference ImportMethodReference (MethodReference mr, ImportContext context)
		{
			if (mr.DeclaringType.Module == m_module)
				return mr;

			if (mr is MethodSpecification)
				return GetMethodSpec (mr, context);

			MethodReference meth = (MethodReference) GetMemberReference (mr);
			if (meth != null)
				return meth;

			meth = new MethodReference (
				mr.Name,
				mr.HasThis,
				mr.ExplicitThis,
				mr.CallingConvention);
			meth.DeclaringType = ImportTypeReference (mr.DeclaringType, context);

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

			context.GenericContext.Method = meth;
			context.GenericContext.Type = meth.DeclaringType.GetOriginalType();

			GenericParameter.CloneInto (mr, meth, context);

			meth.ReturnType.ReturnType = ImportTypeReference (mr.ReturnType.ReturnType, context);

			foreach (ParameterDefinition param in mr.Parameters)
				meth.Parameters.Add (new ParameterDefinition (
					ImportTypeReference (param.ParameterType, context)));

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

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

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

			return null;
		}
	}
}