// Copyright (c) 2011 AlphaSierraPapa for the SharpDevelop Team
// 
// 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.Diagnostics;
using System.Linq;
using Mono.Cecil;
namespace ICSharpCode.Decompiler.ILAst
{
	/// 
	/// Performs inlining transformations.
	/// 
	public class ILInlining
	{
		readonly ILBlock method;
		internal Dictionary numStloc  = new Dictionary();
		internal Dictionary numLdloc  = new Dictionary();
		internal Dictionary numLdloca = new Dictionary();
		
		public ILInlining(ILBlock method)
		{
			this.method = method;
			AnalyzeMethod();
		}
		
		void AnalyzeMethod()
		{
			numStloc.Clear();
			numLdloc.Clear();
			numLdloca.Clear();
			
			// Analyse the whole method
			AnalyzeNode(method);
		}
		
		/// 
		/// For each variable reference, adds  to the num* dicts.
		/// Direction will be 1 for analysis, and -1 when removing a node from analysis.
		/// 
		void AnalyzeNode(ILNode node, int direction = 1)
		{
			ILExpression expr = node as ILExpression;
			if (expr != null) {
				ILVariable locVar = expr.Operand as ILVariable;
				if (locVar != null) {
					if (expr.Code == ILCode.Stloc) {
						numStloc[locVar] = numStloc.GetOrDefault(locVar) + direction;
					} else if (expr.Code == ILCode.Ldloc) {
						numLdloc[locVar] = numLdloc.GetOrDefault(locVar) + direction;
					} else if (expr.Code == ILCode.Ldloca) {
						numLdloca[locVar] = numLdloca.GetOrDefault(locVar) + direction;
					} else {
						throw new NotSupportedException(expr.Code.ToString());
					}
				}
				foreach (ILExpression child in expr.Arguments)
					AnalyzeNode(child, direction);
			} else {
				var catchBlock = node as ILTryCatchBlock.CatchBlock;
				if (catchBlock != null && catchBlock.ExceptionVariable != null) {
					numStloc[catchBlock.ExceptionVariable] = numStloc.GetOrDefault(catchBlock.ExceptionVariable) + direction;
				}
				
				foreach (ILNode child in node.GetChildren())
					AnalyzeNode(child, direction);
			}
		}
		
		public bool InlineAllVariables()
		{
			bool modified = false;
			ILInlining i = new ILInlining(method);
			foreach (ILBlock block in method.GetSelfAndChildrenRecursive())
				modified |= i.InlineAllInBlock(block);
			return modified;
		}
		
		public bool InlineAllInBlock(ILBlock block)
		{
			bool modified = false;
			List body = block.Body;
			if (block is ILTryCatchBlock.CatchBlock && body.Count > 1) {
				ILVariable v = ((ILTryCatchBlock.CatchBlock)block).ExceptionVariable;
				if (v != null && v.IsGenerated) {
					if (numLdloca.GetOrDefault(v) == 0 && numStloc.GetOrDefault(v) == 1 && numLdloc.GetOrDefault(v) == 1) {
						ILVariable v2;
						ILExpression ldException;
						if (body[0].Match(ILCode.Stloc, out v2, out ldException) && ldException.MatchLdloc(v)) {
							body.RemoveAt(0);
							((ILTryCatchBlock.CatchBlock)block).ExceptionVariable = v2;
							modified = true;
						}
					}
				}
			}
			for(int i = 0; i < body.Count - 1;) {
				ILVariable locVar;
				ILExpression expr;
				if (body[i].Match(ILCode.Stloc, out locVar, out expr) && InlineOneIfPossible(block.Body, i, aggressive: false)) {
					modified = true;
					i = Math.Max(0, i - 1); // Go back one step
				} else {
					i++;
				}
			}
			foreach(ILBasicBlock bb in body.OfType()) {
				modified |= InlineAllInBasicBlock(bb);
			}
			return modified;
		}
		
		public bool InlineAllInBasicBlock(ILBasicBlock bb)
		{
			bool modified = false;
			List body = bb.Body;
			for(int i = 0; i < body.Count;) {
				ILVariable locVar;
				ILExpression expr;
				if (body[i].Match(ILCode.Stloc, out locVar, out expr) && InlineOneIfPossible(bb.Body, i, aggressive: false)) {
					modified = true;
					i = Math.Max(0, i - 1); // Go back one step
				} else {
					i++;
				}
			}
			return modified;
		}
		
		/// 
		/// Inlines instructions before pos into block.Body[pos].
		/// 
		/// The number of instructions that were inlined.
		public int InlineInto(List body, int pos, bool aggressive)
		{
			if (pos >= body.Count)
				return 0;
			int count = 0;
			while (--pos >= 0) {
				ILExpression expr = body[pos] as ILExpression;
				if (expr == null || expr.Code != ILCode.Stloc)
					break;
				if (InlineOneIfPossible(body, pos, aggressive))
					count++;
				else
					break;
			}
			return count;
		}
		
		/// 
		/// Aggressively inlines the stloc instruction at block.Body[pos] into the next instruction, if possible.
		/// If inlining was possible; we will continue to inline (non-aggressively) into the the combined instruction.
		/// 
		/// 
		/// After the operation, pos will point to the new combined instruction.
		/// 
		public bool InlineIfPossible(List body, ref int pos)
		{
			if (InlineOneIfPossible(body, pos, true)) {
				pos -= InlineInto(body, pos, false);
				return true;
			}
			return false;
		}
		
		/// 
		/// Inlines the stloc instruction at block.Body[pos] into the next instruction, if possible.
		/// 
		public bool InlineOneIfPossible(List body, int pos, bool aggressive)
		{
			ILVariable v;
			ILExpression inlinedExpression;
			if (body[pos].Match(ILCode.Stloc, out v, out inlinedExpression) && !v.IsPinned) {
				if (InlineIfPossible(v, inlinedExpression, body.ElementAtOrDefault(pos+1), aggressive)) {
					// Assign the ranges of the stloc instruction:
					inlinedExpression.ILRanges.AddRange(((ILExpression)body[pos]).ILRanges);
					// Remove the stloc instruction:
					body.RemoveAt(pos);
					return true;
				} else if (numLdloc.GetOrDefault(v) == 0 && numLdloca.GetOrDefault(v) == 0) {
					// The variable is never loaded
					if (inlinedExpression.HasNoSideEffects()) {
						// Remove completely
						AnalyzeNode(body[pos], -1);
						body.RemoveAt(pos);
						return true;
					} else if (inlinedExpression.CanBeExpressionStatement() && v.IsGenerated) {
						// Assign the ranges of the stloc instruction:
						inlinedExpression.ILRanges.AddRange(((ILExpression)body[pos]).ILRanges);
						// Remove the stloc, but keep the inner expression
						body[pos] = inlinedExpression;
						return true;
					}
				}
			}
			return false;
		}
		
		/// 
		/// Inlines 'expr' into 'next', if possible.
		/// 
		bool InlineIfPossible(ILVariable v, ILExpression inlinedExpression, ILNode next, bool aggressive)
		{
			// ensure the variable is accessed only a single time
			if (numStloc.GetOrDefault(v) != 1)
				return false;
			int ldloc = numLdloc.GetOrDefault(v);
			if (ldloc > 1 || ldloc + numLdloca.GetOrDefault(v) != 1)
				return false;
			
			if (next is ILCondition)
				next = ((ILCondition)next).Condition;
			else if (next is ILWhileLoop)
				next = ((ILWhileLoop)next).Condition;
			
			ILExpression parent;
			int pos;
			if (FindLoadInNext(next as ILExpression, v, inlinedExpression, out parent, out pos) == true) {
				if (ldloc == 0) {
					if (!IsGeneratedValueTypeTemporary((ILExpression)next, parent, pos, v, inlinedExpression))
						return false;
				} else {
					if (!aggressive && !v.IsGenerated && !NonAggressiveInlineInto((ILExpression)next, parent, inlinedExpression))
						return false;
				}
				
				// Assign the ranges of the ldloc instruction:
				inlinedExpression.ILRanges.AddRange(parent.Arguments[pos].ILRanges);
				
				if (ldloc == 0) {
					// it was an ldloca instruction, so we need to use the pseudo-opcode 'addressof' so that the types
					// comes out correctly
					parent.Arguments[pos] = new ILExpression(ILCode.AddressOf, null, inlinedExpression);
				} else {
					parent.Arguments[pos] = inlinedExpression;
				}
				return true;
			}
			return false;
		}
		/// 
		/// Is this a temporary variable generated by the C# compiler for instance method calls on value type values
		/// 
		/// The next top-level expression
		/// The direct parent of the load within 'next'
		/// Index of the load within 'parent'
		/// The variable being inlined.
		/// The expression being inlined
		bool IsGeneratedValueTypeTemporary(ILExpression next, ILExpression parent, int pos, ILVariable v, ILExpression inlinedExpression)
		{
			if (pos == 0 && v.Type != null && v.Type.IsValueType) {
				// Inlining a value type variable is allowed only if the resulting code will maintain the semantics
				// that the method is operating on a copy.
				// Thus, we have to disallow inlining of other locals, fields, array elements, dereferenced pointers
				switch (inlinedExpression.Code) {
					case ILCode.Ldloc:
					case ILCode.Stloc:
					case ILCode.CompoundAssignment:
					case ILCode.Ldelem_Any:
					case ILCode.Ldelem_I:
					case ILCode.Ldelem_I1:
					case ILCode.Ldelem_I2:
					case ILCode.Ldelem_I4:
					case ILCode.Ldelem_I8:
					case ILCode.Ldelem_R4:
					case ILCode.Ldelem_R8:
					case ILCode.Ldelem_Ref:
					case ILCode.Ldelem_U1:
					case ILCode.Ldelem_U2:
					case ILCode.Ldelem_U4:
					case ILCode.Ldobj:
					case ILCode.Ldind_Ref:
						return false;
					case ILCode.Ldfld:
					case ILCode.Stfld:
					case ILCode.Ldsfld:
					case ILCode.Stsfld:
						// allow inlining field access only if it's a readonly field
						FieldDefinition f = ((FieldReference)inlinedExpression.Operand).Resolve();
						if (!(f != null && f.IsInitOnly))
							return false;
						break;
					case ILCode.Call:
					case ILCode.CallGetter:
						// inlining runs both before and after IntroducePropertyAccessInstructions,
						// so we have to handle both 'call' and 'callgetter'
						MethodReference mr = (MethodReference)inlinedExpression.Operand;
						// ensure that it's not an multi-dimensional array getter
						if (mr.DeclaringType is ArrayType)
							return false;
						goto case ILCode.Callvirt;
					case ILCode.Callvirt:
					case ILCode.CallvirtGetter:
						// don't inline foreach loop variables:
						mr = (MethodReference)inlinedExpression.Operand;
						if (mr.Name == "get_Current" && mr.HasThis)
							return false;
						break;
					case ILCode.Castclass:
					case ILCode.Unbox_Any:
						// These are valid, but might occur as part of a foreach loop variable.
						ILExpression arg = inlinedExpression.Arguments[0];
						if (arg.Code == ILCode.CallGetter || arg.Code == ILCode.CallvirtGetter || arg.Code == ILCode.Call || arg.Code == ILCode.Callvirt) {
							mr = (MethodReference)arg.Operand;
							if (mr.Name == "get_Current" && mr.HasThis)
								return false; // looks like a foreach loop variable, so don't inline it
						}
						break;
				}
				
				// inline the compiler-generated variable that are used when accessing a member on a value type:
				switch (parent.Code) {
					case ILCode.Call:
					case ILCode.CallGetter:
					case ILCode.CallSetter:
					case ILCode.Callvirt:
					case ILCode.CallvirtGetter:
					case ILCode.CallvirtSetter:
						MethodReference mr = (MethodReference)parent.Operand;
						return mr.HasThis;
					case ILCode.Stfld:
					case ILCode.Ldfld:
					case ILCode.Ldflda:
					case ILCode.Await:
						return true;
				}
			}
			return false;
		}
		
		/// 
		/// Determines whether a variable should be inlined in non-aggressive mode, even though it is not a generated variable.
		/// 
		/// The next top-level expression
		/// The direct parent of the load within 'next'
		/// The expression being inlined
		bool NonAggressiveInlineInto(ILExpression next, ILExpression parent, ILExpression inlinedExpression)
		{
			if (inlinedExpression.Code == ILCode.DefaultValue)
				return true;
			
			switch (next.Code) {
				case ILCode.Ret:
				case ILCode.Brtrue:
					return parent == next;
				case ILCode.Switch:
					return parent == next || (parent.Code == ILCode.Sub && parent == next.Arguments[0]);
				default:
					return false;
			}
		}
		
		/// 
		/// Gets whether 'expressionBeingMoved' can be inlined into 'expr'.
		/// 
		public bool CanInlineInto(ILExpression expr, ILVariable v, ILExpression expressionBeingMoved)
		{
			ILExpression parent;
			int pos;
			return FindLoadInNext(expr, v, expressionBeingMoved, out parent, out pos) == true;
		}
		
		/// 
		/// Finds the position to inline to.
		/// 
		/// true = found; false = cannot continue search; null = not found
		bool? FindLoadInNext(ILExpression expr, ILVariable v, ILExpression expressionBeingMoved, out ILExpression parent, out int pos)
		{
			parent = null;
			pos = 0;
			if (expr == null)
				return false;
			for (int i = 0; i < expr.Arguments.Count; i++) {
				// Stop when seeing an opcode that does not guarantee that its operands will be evaluated.
				// Inlining in that case might result in the inlined expresion not being evaluted.
				if (i == 1 && (expr.Code == ILCode.LogicAnd || expr.Code == ILCode.LogicOr || expr.Code == ILCode.TernaryOp || expr.Code == ILCode.NullCoalescing))
					return false;
				
				ILExpression arg = expr.Arguments[i];
				
				if ((arg.Code == ILCode.Ldloc || arg.Code == ILCode.Ldloca) && arg.Operand == v) {
					parent = expr;
					pos = i;
					return true;
				}
				bool? r = FindLoadInNext(arg, v, expressionBeingMoved, out parent, out pos);
				if (r != null)
					return r;
			}
			if (IsSafeForInlineOver(expr, expressionBeingMoved))
				return null; // continue searching
			else
				return false; // abort, inlining not possible
		}
		
		/// 
		/// Determines whether it is safe to move 'expressionBeingMoved' past 'expr'
		/// 
		bool IsSafeForInlineOver(ILExpression expr, ILExpression expressionBeingMoved)
		{
			switch (expr.Code) {
				case ILCode.Ldloc:
					ILVariable loadedVar = (ILVariable)expr.Operand;
					if (numLdloca.GetOrDefault(loadedVar) != 0) {
						// abort, inlining is not possible
						return false;
					}
					foreach (ILExpression potentialStore in expressionBeingMoved.GetSelfAndChildrenRecursive()) {
						if (potentialStore.Code == ILCode.Stloc && potentialStore.Operand == loadedVar)
							return false;
					}
					// the expression is loading a non-forbidden variable
					return true;
				case ILCode.Ldloca:
				case ILCode.Ldflda:
				case ILCode.Ldsflda:
				case ILCode.Ldelema:
				case ILCode.AddressOf:
				case ILCode.ValueOf:
				case ILCode.NullableOf:
					// address-loading instructions are safe if their arguments are safe
					foreach (ILExpression arg in expr.Arguments) {
						if (!IsSafeForInlineOver(arg, expressionBeingMoved))
							return false;
					}
					return true;
				default:
					// instructions with no side-effects are safe (except for Ldloc and Ldloca which are handled separately)
					return expr.HasNoSideEffects();
			}
		}
		
		/// 
		/// Runs a very simple form of copy propagation.
		/// Copy propagation is used in two cases:
		/// 1) assignments from arguments to local variables
		///    If the target variable is assigned to only once (so always is that argument) and the argument is never changed (no ldarga/starg),
		///    then we can replace the variable with the argument.
		/// 2) assignments of address-loading instructions to local variables
		/// 
		public void CopyPropagation()
		{
			foreach (ILBlock block in method.GetSelfAndChildrenRecursive()) {
				for (int i = 0; i < block.Body.Count; i++) {
					ILVariable v;
					ILExpression copiedExpr;
					if (block.Body[i].Match(ILCode.Stloc, out v, out copiedExpr)
					    && !v.IsParameter && numStloc.GetOrDefault(v) == 1 && numLdloca.GetOrDefault(v) == 0
					    && CanPerformCopyPropagation(copiedExpr, v))
					{
						// un-inline the arguments of the ldArg instruction
						ILVariable[] uninlinedArgs = new ILVariable[copiedExpr.Arguments.Count];
						for (int j = 0; j < uninlinedArgs.Length; j++) {
							uninlinedArgs[j] = new ILVariable { IsGenerated = true, Name = v.Name + "_cp_" + j };
							block.Body.Insert(i++, new ILExpression(ILCode.Stloc, uninlinedArgs[j], copiedExpr.Arguments[j]));
						}
						
						// perform copy propagation:
						foreach (var expr in method.GetSelfAndChildrenRecursive()) {
							if (expr.Code == ILCode.Ldloc && expr.Operand == v) {
								expr.Code = copiedExpr.Code;
								expr.Operand = copiedExpr.Operand;
								for (int j = 0; j < uninlinedArgs.Length; j++) {
									expr.Arguments.Add(new ILExpression(ILCode.Ldloc, uninlinedArgs[j]));
								}
							}
						}
						
						block.Body.RemoveAt(i);
						if (uninlinedArgs.Length > 0) {
							// if we un-inlined stuff; we need to update the usage counters
							AnalyzeMethod();
						}
						InlineInto(block.Body, i, aggressive: false); // maybe inlining gets possible after the removal of block.Body[i]
						i -= uninlinedArgs.Length + 1;
					}
				}
			}
		}
		
		bool CanPerformCopyPropagation(ILExpression expr, ILVariable copyVariable)
		{
			switch (expr.Code) {
				case ILCode.Ldloca:
				case ILCode.Ldelema:
				case ILCode.Ldflda:
				case ILCode.Ldsflda:
					// All address-loading instructions always return the same value for a given operand/argument combination,
					// so they can be safely copied.
					return true;
				case ILCode.Ldloc:
					ILVariable v = (ILVariable)expr.Operand;
					if (v.IsParameter) {
						// Parameters can be copied only if they aren't assigned to (directly or indirectly via ldarga)
						return numLdloca.GetOrDefault(v) == 0 && numStloc.GetOrDefault(v) == 0;
					} else {
						// Variables are be copied only if both they and the target copy variable are generated,
						// and if the variable has only a single assignment
						return v.IsGenerated && copyVariable.IsGenerated && numLdloca.GetOrDefault(v) == 0 && numStloc.GetOrDefault(v) == 1;
					}
				default:
					return false;
			}
		}
	}
}