#region license
// Copyright (c) 2004, Rodrigo B. de Oliveira (rbo@acm.org)
// All rights reserved.
// 
// Redistribution and use in source and binary forms, with or without modification,
// are permitted provided that the following conditions are met:
// 
//     * Redistributions of source code must retain the above copyright notice,
//     this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above copyright notice,
//     this list of conditions and the following disclaimer in the documentation
//     and/or other materials provided with the distribution.
//     * Neither the name of Rodrigo B. de Oliveira nor the names of its
//     contributors may be used to endorse or promote products derived from this
//     software without specific prior written permission.
// 
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#endregion

namespace Boo.Lang.Compiler.Steps
{
	using System;
	using Boo.Lang.Compiler;
	using Boo.Lang.Compiler.Ast;
	using Boo.Lang.Compiler.TypeSystem;
	
	public class InjectCallableConversions : AbstractVisitorCompilerStep
	{
		IMethod _current;
		
		IType _asyncResultType;
		
		IMethod _asyncResultTypeAsyncDelegateGetter;
		
		Boo.Lang.List _adaptors;
		
		override public void Run()
		{
			if (0 == Errors.Count)
			{
				Initialize();
				Visit(CompileUnit);
			}
		}
		
		void Initialize()
		{
			Type type = typeof( System.Runtime.Remoting.Messaging.AsyncResult);
			_asyncResultType = TypeSystemServices.Map(type);
			_asyncResultTypeAsyncDelegateGetter = (IMethod)TypeSystemServices.Map(type.GetProperty("AsyncDelegate").GetGetMethod());
			_adaptors = new Boo.Lang.List();
		}
		
		override public void Dispose()
		{
			_asyncResultType = null;
			_asyncResultTypeAsyncDelegateGetter = null;
			_adaptors = null;
			base.Dispose();
		}
		
		override public void OnMethod(Method node)
		{
			_current = (IMethod)GetEntity(node);
			Visit(node.Body);
		}
		
		bool HasReturnType(IMethod method)
		{
			return TypeSystemServices.VoidType != method.ReturnType;
		}
		
		bool IsMethodReference(Expression node)
		{
			return EntityType.Method == node.Entity.EntityType;
		}
		
		bool IsNotTargetOfMethodInvocation(Expression node)
		{
			MethodInvocationExpression mie = node.ParentNode as MethodInvocationExpression;
			if (null != mie)
			{
				return mie.Target != node;
			}
			return true;
		}
		
		bool IsStandaloneMethodReference(Expression node)
		{
			return
				(node is ReferenceExpression) &&
				IsMethodReference(node) &&
				IsNotTargetOfMethodInvocation(node);
		}
		
		override public void LeaveReturnStatement(ReturnStatement node)
		{
			if (HasReturnType(_current))
			{
				Expression expression = node.Expression;
				if (null != expression)
				{
					Expression newExpression = Convert(_current.ReturnType, expression);
					if (null != newExpression)
					{
						node.Expression = newExpression;
					}
				}
			}			
		}
		
		override public void LeaveExpressionPair(ExpressionPair pair)
		{
			Expression converted = ConvertExpression(pair.First);
			if (null != converted)
			{
				pair.First = converted;
			}
			
			converted = ConvertExpression(pair.Second);
			if (null != converted)
			{
				pair.Second = converted;
			}
		}
		
		override public void LeaveListLiteralExpression(ListLiteralExpression node)
		{
			ConvertExpressions(node.Items);
		}
		
		override public void LeaveArrayLiteralExpression(ArrayLiteralExpression node)
		{
			ConvertExpressions(node.Items);
		}
		
		override public void LeaveMethodInvocationExpression(MethodInvocationExpression node)
		{
			ICallableType type = node.Target.ExpressionType as ICallableType;
			if (null == type)
			{
				return;
			}
			
			IParameter[] parameters = type.GetSignature().Parameters;
			ExpressionCollection arguments = node.Arguments;
			for (int i=0; i<parameters.Length; ++i)
			{
				Expression newArgument = Convert(parameters[i].Type, arguments[i]);
				if (null != newArgument)
				{
					arguments.ReplaceAt(i, newArgument);
				}
			}
		}
		
		override public void LeaveMemberReferenceExpression(MemberReferenceExpression node)
		{
			if (IsEndInvokeOnStandaloneMethodReference(node) &&
				AstUtil.IsTargetOfMethodInvocation(node))
			{
				ReplaceEndInvokeTargetByGetAsyncDelegate((MethodInvocationExpression)node.ParentNode);
			}
			else
			{
				Expression newTarget = ConvertExpression(node.Target);
				if (null != newTarget)
				{
					node.Target = newTarget;
				}
			}
		}
		
		override public void LeaveCastExpression(CastExpression node)
		{
			Expression newExpression = Convert(node.ExpressionType, node.Target);
			if (null != newExpression)
			{
				node.ParentNode.Replace(node, newExpression);
			}
		}
		
		override public void LeaveBinaryExpression(BinaryExpression node)
		{
			if (BinaryOperatorType.Assign == node.Operator)
			{
				Expression newRight = Convert(node.Left.ExpressionType, node.Right);
				if (null != newRight)
				{
					node.Right = newRight;
				}
			}
		}
		
		override public void LeaveGeneratorExpression(GeneratorExpression node)
		{
			Expression newExpression = Convert(
																GetConcreteExpressionType(node.Expression),
																node.Expression);
			if (null != newExpression)
			{
				node.Expression = newExpression;
			}
		}
		
		void ConvertExpressions(ExpressionCollection items)
		{
			for (int i=0; i<items.Count; ++i)
			{
				Expression converted = ConvertExpression(items[i]);
				if (null != converted)
				{
					items.ReplaceAt(i, converted);
				}
			}
		}
		
		Expression ConvertExpression(Expression expression)
		{
			return Convert(expression.ExpressionType, expression);
		}
		
		Expression Convert(IType expectedType, Expression argument)
		{
			if (IsStandaloneMethodReference(argument))
			{
				if (IsCallableType(expectedType))
				{
					ICallableType argumentType = (ICallableType)GetExpressionType(argument);
					if (argumentType.GetSignature() != 
						((ICallableType)expectedType).GetSignature())
					{						
						return Adapt((ICallableType)expectedType,
							CreateDelegate(GetConcreteExpressionType(argument), argument));
					}
					return CreateDelegate(expectedType, argument);
				}
				else
				{
					return CreateDelegate(GetConcreteExpressionType(argument), argument);
				}
			}
			else
			{				
				if (IsCallableType(expectedType))
				{
					IType argumentType = GetExpressionType(argument);
					if (Null.Default != argumentType &&
						expectedType != argumentType)
					{						
						return Adapt((ICallableType)expectedType, argument);
					}
				}
			}
			return null;
		}
		
		Expression Adapt(ICallableType expected, Expression callable)
		{
			ICallableType actual = GetExpressionType(callable) as ICallableType;
			if (null == actual)
			{
				// TODO: should we adapt System.Object, System.Delegate,
				// System.MulticastDelegate and ICallable as well?
				return null;
			}
			ClassDefinition adaptor = GetAdaptor(expected, actual);
			Method adapt = (Method)adaptor.Members["Adapt"];						
			return CodeBuilder.CreateMethodInvocation((IMethod)adapt.Entity, callable);
		}
		
		ClassDefinition GetAdaptor(ICallableType to, ICallableType from)
		{
			ClassDefinition adaptor = FindAdaptor(to, from);
			if (null == adaptor)
			{
				adaptor = CreateAdaptor(to, from);
			}
			return adaptor;
		}
		
		class AdaptorRecord
		{			
			public readonly ICallableType To;
			public readonly ICallableType From;
			public readonly ClassDefinition Adaptor;
			
			public AdaptorRecord(ICallableType to, ICallableType from, ClassDefinition adaptor)
			{
				To = to;
				From = from;				
				Adaptor = adaptor;
			}
		}
		
		ClassDefinition FindAdaptor(ICallableType to, ICallableType from)
		{
			foreach (AdaptorRecord record in _adaptors)
			{
				if (from == record.From && to == record.To)
				{
					return record.Adaptor;
				}
			}
			return null;
		}
		
		ClassDefinition CreateAdaptor(ICallableType to, ICallableType from)
		{
			BooClassBuilder adaptor = CodeBuilder.CreateClass("__adaptor" + _adaptors.Count + "__");
			adaptor.AddBaseType(TypeSystemServices.ObjectType);
			adaptor.Modifiers = TypeMemberModifiers.Final|TypeMemberModifiers.Internal;
			
			Field callable = adaptor.AddField("__callable", from);
			
			BooMethodBuilder constructor = adaptor.AddConstructor();
			ParameterDeclaration param = constructor.AddParameter("from_", from);
			constructor.Body.Add(
				CodeBuilder.CreateSuperConstructorInvocation(TypeSystemServices.ObjectType));			
			constructor.Body.Add(
				CodeBuilder.CreateAssignment(
					CodeBuilder.CreateReference(callable),
					CodeBuilder.CreateReference(param)));			
					
			CallableSignature signature = to.GetSignature();
			BooMethodBuilder invoke = adaptor.AddMethod("Invoke", signature.ReturnType);
			foreach (IParameter parameter in signature.Parameters)
			{
				invoke.AddParameter(parameter.Name, parameter.Type);
			}
			MethodInvocationExpression mie = CodeBuilder.CreateMethodInvocation(
							CodeBuilder.CreateReference(callable),
							GetInvokeMethod(from));
			int fromParameterCount = from.GetSignature().Parameters.Length;
			for (int i=0; i<fromParameterCount; ++i)
			{
				mie.Arguments.Add(
					CodeBuilder.CreateReference(invoke.Parameters[i]));
			}							
			if (signature.ReturnType != TypeSystemServices.VoidType)
			{
				invoke.Body.Add(new ReturnStatement(mie));
			}			
			else
			{
				invoke.Body.Add(mie);
			}
			
			BooMethodBuilder adapt = adaptor.AddMethod("Adapt", to);
			adapt.Modifiers = TypeMemberModifiers.Static|TypeMemberModifiers.Public;
			param = adapt.AddParameter("from_", from);
			adapt.Body.Add(
				new ReturnStatement(
					CodeBuilder.CreateConstructorInvocation(
						to.GetConstructors()[0],
						CodeBuilder.CreateConstructorInvocation(
							(IConstructor)constructor.Entity,
							CodeBuilder.CreateReference(param)),
						CodeBuilder.CreateAddressOfExpression(invoke.Entity))));					
			
			RegisterAdaptor(to, from, adaptor.ClassDefinition);
			
			return adaptor.ClassDefinition;
		}
		
		void RegisterAdaptor(ICallableType to, ICallableType from, ClassDefinition adaptor)
		{
			_adaptors.Add(new AdaptorRecord(to, from, adaptor));
			TypeSystemServices.GetAnonymousTypesModule().Members.Add(adaptor);
		}
		
		bool IsEndInvokeOnStandaloneMethodReference(MemberReferenceExpression node)
		{
			if (IsStandaloneMethodReference(node.Target))
			{
				InternalCallableType type = (InternalCallableType)node.Target.ExpressionType;
				return type.GetEndInvokeMethod() == node.Entity;
			}
			return false;
		}
		
		void ReplaceEndInvokeTargetByGetAsyncDelegate(MethodInvocationExpression node)
		{
			Expression asyncResult = node.Arguments[0];
			MemberReferenceExpression endInvoke = (MemberReferenceExpression)node.Target;
			IType callableType = ((IMember)endInvoke.Entity).DeclaringType;
			
			endInvoke.Target = CodeBuilder.CreateCast(callableType,
									CodeBuilder.CreateMethodInvocation(
										CodeBuilder.CreateCast(_asyncResultType, asyncResult.CloneNode()),
										_asyncResultTypeAsyncDelegateGetter));
										
		}
		
		bool IsNull(IType type)
		{
			return EntityType.Null == type.EntityType;
		}
		
		bool IsCallableType(IType type)
		{
			return type is ICallableType;
		}
		
		Expression CreateDelegate(IType type, Expression source)
		{
			IMethod method = (IMethod)GetEntity(source);
			
			MethodInvocationExpression constructor = new MethodInvocationExpression(source.LexicalInfo);
			constructor.Target = new ReferenceExpression(type.FullName);
			
			if (method.IsStatic)
			{
				constructor.Arguments.Add(new NullLiteralExpression());
			}
			else
			{
				constructor.Arguments.Add(((MemberReferenceExpression)source).Target);
			}
			
			constructor.Arguments.Add(CodeBuilder.CreateAddressOfExpression(method));
			Bind(constructor.Target, type.GetConstructors()[0]);
			BindExpressionType(constructor, type);
			
			return constructor;
		}
		
		IMethod GetInvokeMethod(ICallableType type)
		{
			return NameResolutionService.ResolveMethod(type, "Invoke");
		}
	}
}
