// 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
{
	/// 
	/// IL AST transformation that introduces array, object and collection initializers.
	/// 
	partial class ILAstOptimizer
	{
		#region Array Initializers
		bool TransformArrayInitializers(List body, ILExpression expr, int pos)
		{
			ILVariable v, v3;
			ILExpression newarrExpr;
			TypeReference elementType;
			ILExpression lengthExpr;
			int arrayLength;
			if (expr.Match(ILCode.Stloc, out v, out newarrExpr) &&
			    newarrExpr.Match(ILCode.Newarr, out elementType, out lengthExpr) &&
			    lengthExpr.Match(ILCode.Ldc_I4, out arrayLength) &&
			    arrayLength > 0) {
				ILExpression[] newArr;
				int initArrayPos;
				if (ForwardScanInitializeArrayRuntimeHelper(body, pos + 1, v, elementType, arrayLength, out newArr, out initArrayPos)) {
					var arrayType = new ArrayType(elementType, 1);
					arrayType.Dimensions[0] = new ArrayDimension(0, arrayLength);
					body[pos] = new ILExpression(ILCode.Stloc, v, new ILExpression(ILCode.InitArray, arrayType, newArr));
					body.RemoveAt(initArrayPos);
				}
				// Put in a limit so that we don't consume too much memory if the code allocates a huge array
				// and populates it extremely sparsly. However, 255 "null" elements in a row actually occur in the Mono C# compiler!
				const int maxConsecutiveDefaultValueExpressions = 300;
				List operands = new List();
				int numberOfInstructionsToRemove = 0;
				for (int j = pos + 1; j < body.Count; j++) {
					ILExpression nextExpr = body[j] as ILExpression;
					int arrayPos;
					if (nextExpr != null &&
					    nextExpr.Code.IsStoreToArray() &&
					    nextExpr.Arguments[0].Match(ILCode.Ldloc, out v3) &&
					    v == v3 &&
					    nextExpr.Arguments[1].Match(ILCode.Ldc_I4, out arrayPos) &&
					    arrayPos >= operands.Count &&
					    arrayPos <= operands.Count + maxConsecutiveDefaultValueExpressions &&
					    !nextExpr.Arguments[2].ContainsReferenceTo(v3))
					{
						while (operands.Count < arrayPos)
							operands.Add(new ILExpression(ILCode.DefaultValue, elementType));
						operands.Add(nextExpr.Arguments[2]);
						numberOfInstructionsToRemove++;
					} else {
						break;
					}
				}
				if (operands.Count == arrayLength) {
					var arrayType = new ArrayType(elementType, 1);
					arrayType.Dimensions[0] = new ArrayDimension(0, arrayLength);
					expr.Arguments[0] = new ILExpression(ILCode.InitArray, arrayType, operands);
					body.RemoveRange(pos + 1, numberOfInstructionsToRemove);
					new ILInlining(method).InlineIfPossible(body, ref pos);
					return true;
				}
			}
			return false;
		}
		bool TransformMultidimensionalArrayInitializers(List body, ILExpression expr, int pos)
		{
			ILVariable v;
			ILExpression newarrExpr;
			MethodReference ctor;
			List ctorArgs;
			ArrayType arrayType;
			if (expr.Match(ILCode.Stloc, out v, out newarrExpr) &&
			    newarrExpr.Match(ILCode.Newobj, out ctor, out ctorArgs) &&
			    (arrayType = (ctor.DeclaringType as ArrayType)) != null &&
			    arrayType.Rank == ctorArgs.Count) {
				// Clone the type, so we can muck about with the Dimensions
				arrayType = new ArrayType(arrayType.ElementType, arrayType.Rank);
				var arrayLengths = new int[arrayType.Rank];
				for (int i = 0; i < arrayType.Rank; i++) {
					if (!ctorArgs[i].Match(ILCode.Ldc_I4, out arrayLengths[i])) return false;
					if (arrayLengths[i] <= 0) return false;
					arrayType.Dimensions[i] = new ArrayDimension(0, arrayLengths[i]);
				}
				var totalElements = arrayLengths.Aggregate(1, (t, l) => t * l);
				ILExpression[] newArr;
				int initArrayPos;
				if (ForwardScanInitializeArrayRuntimeHelper(body, pos + 1, v, arrayType, totalElements, out newArr, out initArrayPos)) {
					body[pos] = new ILExpression(ILCode.Stloc, v, new ILExpression(ILCode.InitArray, arrayType, newArr));
					body.RemoveAt(initArrayPos);
					return true;
				}
			}
			return false;
		}
		bool ForwardScanInitializeArrayRuntimeHelper(List body, int pos, ILVariable array, TypeReference arrayType, int arrayLength, out ILExpression[] values, out int foundPos)
		{
			ILVariable v2;
			MethodReference methodRef;
			ILExpression methodArg1;
			ILExpression methodArg2;
			FieldReference fieldRef;
			if (body.ElementAtOrDefault(pos).Match(ILCode.Call, out methodRef, out methodArg1, out methodArg2) &&
			    methodRef.DeclaringType.FullName == "System.Runtime.CompilerServices.RuntimeHelpers" &&
			    methodRef.Name == "InitializeArray" &&
			    methodArg1.Match(ILCode.Ldloc, out v2) &&
			    array == v2 &&
			    methodArg2.Match(ILCode.Ldtoken, out fieldRef))
			{
				FieldDefinition fieldDef = fieldRef.ResolveWithinSameModule();
				if (fieldDef != null && fieldDef.InitialValue != null) {
					ILExpression[] newArr = new ILExpression[arrayLength];
					if (DecodeArrayInitializer(arrayType.GetElementType(), fieldDef.InitialValue, newArr))
					{
						values = newArr;
						foundPos = pos;
						return true;
					}
				}
			}
			values = null;
			foundPos = -1;
			return false;
		}
		static bool DecodeArrayInitializer(TypeReference elementTypeRef, byte[] initialValue, ILExpression[] output)
		{
			TypeCode elementType = TypeAnalysis.GetTypeCode(elementTypeRef);
			switch (elementType) {
				case TypeCode.Boolean:
				case TypeCode.Byte:
					return DecodeArrayInitializer(initialValue, output, elementType, (d, i) => (int)d[i]);
				case TypeCode.SByte:
					return DecodeArrayInitializer(initialValue, output, elementType, (d, i) => (int)unchecked((sbyte)d[i]));
				case TypeCode.Int16:
					return DecodeArrayInitializer(initialValue, output, elementType, (d, i) => (int)BitConverter.ToInt16(d, i));
				case TypeCode.Char:
				case TypeCode.UInt16:
					return DecodeArrayInitializer(initialValue, output, elementType, (d, i) => (int)BitConverter.ToUInt16(d, i));
				case TypeCode.Int32:
				case TypeCode.UInt32:
					return DecodeArrayInitializer(initialValue, output, elementType, BitConverter.ToInt32);
				case TypeCode.Int64:
				case TypeCode.UInt64:
					return DecodeArrayInitializer(initialValue, output, elementType, BitConverter.ToInt64);
				case TypeCode.Single:
					return DecodeArrayInitializer(initialValue, output, elementType, BitConverter.ToSingle);
				case TypeCode.Double:
					return DecodeArrayInitializer(initialValue, output, elementType, BitConverter.ToDouble);
				case TypeCode.Object:
					var typeDef = elementTypeRef.ResolveWithinSameModule();
					if (typeDef != null && typeDef.IsEnum)
						return DecodeArrayInitializer(typeDef.GetEnumUnderlyingType(), initialValue, output);
					return false;
				default:
					return false;
			}
		}
		static bool DecodeArrayInitializer(byte[] initialValue, ILExpression[] output, TypeCode elementType, Func decoder)
		{
			int elementSize = ElementSizeOf(elementType);
			if (initialValue.Length < (output.Length * elementSize))
				return false;
			ILCode code = LoadCodeFor(elementType);
			for (int i = 0; i < output.Length; i++)
				output[i] = new ILExpression(code, decoder(initialValue, i * elementSize));
			return true;
		}
		private static ILCode LoadCodeFor(TypeCode elementType)
		{
			switch (elementType) {
				case TypeCode.Boolean:
				case TypeCode.Byte:
				case TypeCode.SByte:
				case TypeCode.Char:
				case TypeCode.Int16:
				case TypeCode.UInt16:
				case TypeCode.Int32:
				case TypeCode.UInt32:
					return ILCode.Ldc_I4;
				case TypeCode.Int64:
				case TypeCode.UInt64:
					return ILCode.Ldc_I8;
				case TypeCode.Single:
					return ILCode.Ldc_R4;
				case TypeCode.Double:
					return ILCode.Ldc_R8;
				default:
					throw new ArgumentOutOfRangeException("elementType");					
			}
		}
		private static int ElementSizeOf(TypeCode elementType)
		{
			switch (elementType) {
				case TypeCode.Boolean:
				case TypeCode.Byte:
				case TypeCode.SByte:
					return 1;
				case TypeCode.Char:
				case TypeCode.Int16:
				case TypeCode.UInt16:
					return 2;
				case TypeCode.Int32:
				case TypeCode.UInt32:
				case TypeCode.Single:
					return 4;
				case TypeCode.Int64:
				case TypeCode.UInt64:
				case TypeCode.Double:
					return 8;
				default:
					throw new ArgumentOutOfRangeException("elementType");
			}
		}
		#endregion
		/// 
		/// Handles both object and collection initializers.
		/// 
		bool TransformObjectInitializers(List body, ILExpression expr, int pos)
		{
			if (!context.Settings.ObjectOrCollectionInitializers)
				return false;
			Debug.Assert(body[pos] == expr); // should be called for top-level expressions only
			ILVariable v;
			ILExpression newObjExpr;
			TypeReference newObjType;
			bool isValueType;
			MethodReference ctor;
			List ctorArgs;
			if (expr.Match(ILCode.Stloc, out v, out newObjExpr)) {
				if (newObjExpr.Match(ILCode.Newobj, out ctor, out ctorArgs)) {
					// v = newObj(ctor, ctorArgs)
					newObjType = ctor.DeclaringType;
					isValueType = false;
				} else if (newObjExpr.Match(ILCode.DefaultValue, out newObjType)) {
					// v = defaultvalue(type)
					isValueType = true;
				} else {
					return false;
				}
			} else if (expr.Match(ILCode.Call, out ctor, out ctorArgs)) {
				// call(SomeStruct::.ctor, ldloca(v), remainingArgs)
				if (ctorArgs.Count > 0 && ctorArgs[0].Match(ILCode.Ldloca, out v)) {
					isValueType = true;
					newObjType = ctor.DeclaringType;
					ctorArgs = new List(ctorArgs);
					ctorArgs.RemoveAt(0);
					newObjExpr = new ILExpression(ILCode.Newobj, ctor, ctorArgs);
				} else {
					return false;
				}
			} else {
				return false;
			}
			if (newObjType.IsValueType != isValueType)
				return false;
			
			int originalPos = pos;
			// don't use object initializer syntax for closures
			if (Ast.Transforms.DelegateConstruction.IsPotentialClosure(context, newObjType.ResolveWithinSameModule()))
				return false;
			ILExpression initializer = ParseObjectInitializer(body, ref pos, v, newObjExpr, IsCollectionType(newObjType), isValueType);
			if (initializer.Arguments.Count == 1) // only newobj argument, no initializer elements
				return false;
			int totalElementCount = pos - originalPos - 1; // totalElementCount: includes elements from nested collections
			Debug.Assert(totalElementCount >= initializer.Arguments.Count - 1);
			// Verify that we can inline 'v' into the next instruction:
			if (pos >= body.Count)
				return false; // reached end of block, but there should be another instruction which consumes the initialized object
			ILInlining inlining = new ILInlining(method);
			if (isValueType) {
				// one ldloc for the use of the initialized object
				if (inlining.numLdloc.GetOrDefault(v) != 1)
					return false;
				// one ldloca for each initializer argument, and also for the ctor call (if it exists)
				if (inlining.numLdloca.GetOrDefault(v) != totalElementCount + (expr.Code == ILCode.Call ? 1 : 0))
					return false;
				// one stloc for the initial store (if no ctor call was used)
				if (inlining.numStloc.GetOrDefault(v) != (expr.Code == ILCode.Call ? 0 : 1))
					return false;
			} else {
				// one ldloc for each initializer argument, and another ldloc for the use of the initialized object
				if (inlining.numLdloc.GetOrDefault(v) != totalElementCount + 1)
					return false;
				if (!(inlining.numStloc.GetOrDefault(v) == 1 && inlining.numLdloca.GetOrDefault(v) == 0))
					return false;
			}
			ILExpression nextExpr = body[pos] as ILExpression;
			if (!inlining.CanInlineInto(nextExpr, v, initializer))
				return false;
			if (expr.Code == ILCode.Stloc) {
				expr.Arguments[0] = initializer;
			} else {
				Debug.Assert(expr.Code == ILCode.Call);
				expr.Code = ILCode.Stloc;
				expr.Operand = v;
				expr.Arguments.Clear();
				expr.Arguments.Add(initializer);
			}
			// remove all the instructions that were pulled into the initializer
			body.RemoveRange(originalPos + 1, pos - originalPos - 1);
			// now that we know that it's an object initializer, change all the first arguments to 'InitializedObject'
			ChangeFirstArgumentToInitializedObject(initializer);
			inlining = new ILInlining(method);
			inlining.InlineIfPossible(body, ref originalPos);
			return true;
		}
		/// 
		/// Gets whether the type supports collection initializers.
		/// 
		static bool IsCollectionType(TypeReference tr)
		{
			if (tr == null)
				return false;
			TypeDefinition td = tr.Resolve();
			while (td != null) {
				if (td.Interfaces.Any(intf => intf.Name == "IEnumerable" && intf.Namespace == "System.Collections"))
					return true;
				td = td.BaseType != null ? td.BaseType.Resolve() : null;
			}
			return false;
		}
		/// 
		/// Gets whether 'expr' represents a setter in an object initializer.
		/// ('CallvirtSetter(Property, v, value)')
		/// 
		static bool IsSetterInObjectInitializer(ILExpression expr)
		{
			if (expr == null)
				return false;
			if (expr.Code == ILCode.CallvirtSetter || expr.Code == ILCode.CallSetter || expr.Code == ILCode.Stfld) {
				return expr.Arguments.Count == 2;
			}
			return false;
		}
		/// 
		/// Gets whether 'expr' represents the invocation of an 'Add' method in a collection initializer.
		/// 
		static bool IsAddMethodCall(ILExpression expr)
		{
			MethodReference addMethod;
			List args;
			if (expr.Match(ILCode.Callvirt, out addMethod, out args) || expr.Match(ILCode.Call, out addMethod, out args)) {
				if (addMethod.Name == "Add" && addMethod.HasThis) {
					return args.Count >= 2;
				}
			}
			return false;
		}
		/// 
		/// Parses an object initializer.
		/// 
		/// ILAst block
		/// 
		/// Input: position of the instruction assigning to 'v'.
		/// Output: first position after the object initializer
		/// 
		/// The variable that holds the object being initialized
		/// The newobj instruction
		/// InitObject instruction
		ILExpression ParseObjectInitializer(List body, ref int pos, ILVariable v, ILExpression newObjExpr, bool isCollection, bool isValueType)
		{
			// Take care not to modify any existing ILExpressions in here.
			// We just construct new ones around the old ones, any modifications must wait until the whole
			// object/collection initializer was analyzed.
			ILExpression objectInitializer = new ILExpression(isCollection ? ILCode.InitCollection : ILCode.InitObject, null, newObjExpr);
			List initializerStack = new List();
			initializerStack.Add(objectInitializer);
			while (++pos < body.Count) {
				ILExpression nextExpr = body[pos] as ILExpression;
				if (IsSetterInObjectInitializer(nextExpr)) {
					if (!AdjustInitializerStack(initializerStack, nextExpr.Arguments[0], v, false, isValueType)) {
						CleanupInitializerStackAfterFailedAdjustment(initializerStack);
						break;
					}
					initializerStack[initializerStack.Count - 1].Arguments.Add(nextExpr);
				} else if (IsAddMethodCall(nextExpr)) {
					if (!AdjustInitializerStack(initializerStack, nextExpr.Arguments[0], v, true, isValueType)) {
						CleanupInitializerStackAfterFailedAdjustment(initializerStack);
						break;
					}
					initializerStack[initializerStack.Count - 1].Arguments.Add(nextExpr);
				} else {
					// can't match any more initializers: end of object initializer
					break;
				}
			}
			return objectInitializer;
		}
		static bool AdjustInitializerStack(List initializerStack, ILExpression argument, ILVariable v, bool isCollection, bool isValueType)
		{
			// Argument is of the form 'getter(getter(...(v)))'
			// Unpack it into a list of getters:
			List getters = new List();
			while (argument.Code == ILCode.CallvirtGetter || argument.Code == ILCode.CallGetter || argument.Code == ILCode.Ldfld) {
				getters.Add(argument);
				if (argument.Arguments.Count != 1)
					return false;
				argument = argument.Arguments[0];
			}
			// Ensure that the final argument is 'v'
			if (isValueType) {
				ILVariable loadedVar;
				if (!(argument.Match(ILCode.Ldloca, out loadedVar) && loadedVar == v))
					return false;
			} else {
				if (!argument.MatchLdloc(v))
					return false;
			}
			// Now compare the getters with those that are currently active on the initializer stack:
			int i;
			for (i = 1; i <= Math.Min(getters.Count, initializerStack.Count - 1); i++) {
				ILExpression g1 = initializerStack[i].Arguments[0]; // getter stored in initializer
				ILExpression g2 = getters[getters.Count - i]; // matching getter from argument
				if (g1.Operand != g2.Operand) {
					// operands differ, so we abort the comparison
					break;
				}
			}
			// Remove all initializers from the stack that were not matched with one from the argument:
			initializerStack.RemoveRange(i, initializerStack.Count - i);
			// Now create new initializers for the remaining arguments:
			for (; i <= getters.Count; i++) {
				ILExpression g = getters[getters.Count - i];
				MemberReference mr = (MemberReference)g.Operand;
				TypeReference returnType;
				if (mr is FieldReference)
					returnType = TypeAnalysis.GetFieldType((FieldReference)mr);
				else
					returnType = TypeAnalysis.SubstituteTypeArgs(((MethodReference)mr).ReturnType, mr);
				ILExpression nestedInitializer = new ILExpression(
					IsCollectionType(returnType) ? ILCode.InitCollection : ILCode.InitObject,
					null, g);
				// add new initializer to its parent:
				ILExpression parentInitializer = initializerStack[initializerStack.Count - 1];
				if (parentInitializer.Code == ILCode.InitCollection) {
					// can't add children to collection initializer
					if (parentInitializer.Arguments.Count == 1) {
						// convert empty collection initializer to object initializer
						parentInitializer.Code = ILCode.InitObject;
					} else {
						return false;
					}
				}
				parentInitializer.Arguments.Add(nestedInitializer);
				initializerStack.Add(nestedInitializer);
			}
			ILExpression lastInitializer = initializerStack[initializerStack.Count - 1];
			if (isCollection) {
				return lastInitializer.Code == ILCode.InitCollection;
			} else {
				if (lastInitializer.Code == ILCode.InitCollection) {
					if (lastInitializer.Arguments.Count == 1) {
						// convert empty collection initializer to object initializer
						lastInitializer.Code = ILCode.InitObject;
						return true;
					} else {
						return false;
					}
				} else {
					return true;
				}
			}
		}
		static void CleanupInitializerStackAfterFailedAdjustment(List initializerStack)
		{
			// There might be empty nested initializers left over; so we'll remove those:
			while (initializerStack.Count > 1 && initializerStack[initializerStack.Count - 1].Arguments.Count == 1) {
				ILExpression parent = initializerStack[initializerStack.Count - 2];
				Debug.Assert(parent.Arguments.Last() == initializerStack[initializerStack.Count - 1]);
				parent.Arguments.RemoveAt(parent.Arguments.Count - 1);
				initializerStack.RemoveAt(initializerStack.Count - 1);
			}
		}
		static void ChangeFirstArgumentToInitializedObject(ILExpression initializer)
		{
			// Go through all elements in the initializer (so skip the newobj-instr. at the start)
			for (int i = 1; i < initializer.Arguments.Count; i++) {
				ILExpression element = initializer.Arguments[i];
				if (element.Code == ILCode.InitCollection || element.Code == ILCode.InitObject) {
					// nested collection/object initializer
					ILExpression getCollection = element.Arguments[0];
					getCollection.Arguments[0] = new ILExpression(ILCode.InitializedObject, null);
					ChangeFirstArgumentToInitializedObject(element); // handle the collection elements
				} else {
					element.Arguments[0] = new ILExpression(ILCode.InitializedObject, null);
				}
			}
		}
	}
}