// Copyright (c) 2010-2013 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;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading;
using ICSharpCode.NRefactory.CSharp;
using ICSharpCode.NRefactory.CSharp.Resolver;
using ICSharpCode.NRefactory.Semantics;
using ICSharpCode.NRefactory.TypeSystem;

namespace ICSharpCode.NRefactory.ConsistencyCheck
{
	public class RandomizedOrderResolverTest
	{
		static Random sharedRnd = new Random();
		CSharpAstResolver resolver;
		CSharpAstResolver resolveAllResolver;
		
		public static void RunTest(CSharpFile file)
		{
			int seed;
			lock (sharedRnd) {
				seed = sharedRnd.Next();
			}
			Random rnd = new Random(seed);
			var test = new RandomizedOrderResolverTest();
			// Resolve all nodes, but in a random order without using a navigator.
			test.resolver = new CSharpAstResolver(file.Project.Compilation, file.SyntaxTree, file.UnresolvedTypeSystemForFile);
			// For comparing whether the results are equivalent, we also use a normal 'resolve all' resolver:
			test.resolveAllResolver = new CSharpAstResolver(file.Project.Compilation, file.SyntaxTree, file.UnresolvedTypeSystemForFile);
			test.resolveAllResolver.ApplyNavigator(new ResolveAllNavigator(), CancellationToken.None);
			// Prepare list of actions that we need to verify:
			var actions = new List<Func<bool>>();
			bool checkResults = rnd.Next(0, 2) == 0;
			bool checkStateBefore = rnd.Next(0, 2) == 0;
			bool checkStateAfter = rnd.Next(0, 2) == 0;
			bool checkConversion = rnd.Next(0, 2) == 0;
			bool checkExpectedType = rnd.Next(0, 2) == 0;
			foreach (var _node in file.SyntaxTree.DescendantsAndSelf) {
				var node = _node;
				if (CSharpAstResolver.IsUnresolvableNode(node))
					continue;
				if (checkResults)
					actions.Add(() => test.CheckResult(node));
				if (checkStateBefore)
					actions.Add(() => test.CheckStateBefore(node));
				if (checkStateAfter)
					actions.Add(() => test.CheckStateAfter(node));
				var expr = node as Expression;
				if (expr != null) {
					if (checkConversion)
						actions.Add(() => test.CheckConversion(expr));
					if (checkExpectedType)
						actions.Add(() => test.CheckExpectedType(expr));
				}
			}
			
			// Fisher-Yates shuffle
			for (int i = actions.Count - 1; i > 0; i--) {
				int j = rnd.Next(0, i);
				var tmp = actions[i];
				actions[i] = actions[j];
				actions[j] = tmp;
			}
			
			foreach (var action in actions) {
				if (!action()) {
					Console.WriteLine("Seed for this file was: " + seed);
					break;
				}
			}
		}
		
		bool CheckResult(AstNode node)
		{
			ResolveResult expectedResult = resolveAllResolver.Resolve(node);
			ResolveResult actualResult = resolver.Resolve(node);
			if (IsEqualResolveResult(expectedResult, actualResult))
				return true;
			Console.WriteLine("Different resolve results for '{0}' at {1} in {2}:", node, node.StartLocation, node.GetRegion().FileName);
			Console.WriteLine(" expected: " + expectedResult);
			Console.WriteLine(" actual:   " + actualResult);
			return false;
		}
		
		bool CheckStateBefore(AstNode node)
		{
			var expectedState = resolveAllResolver.GetResolverStateBefore(node);
			var actualState = resolver.GetResolverStateBefore(node);
			if (IsEqualResolverState(expectedState, actualState))
				return true;
			Console.WriteLine("Different resolver states before '{0}' at {1} in {2}.", node, node.StartLocation, node.GetRegion().FileName);
			return false;
		}
		
		bool CheckStateAfter(AstNode node)
		{
			var expectedState = resolveAllResolver.GetResolverStateAfter(node);
			var actualState = resolver.GetResolverStateAfter(node);
			if (IsEqualResolverState(expectedState, actualState))
				return true;
			Console.WriteLine("Different resolver states after '{0}' at {1} in {2}.", node, node.StartLocation, node.GetRegion().FileName);
			return false;
		}
		
		bool CheckConversion(Expression node)
		{
			Conversion expectedConversion = resolveAllResolver.GetConversion(node);
			Conversion actualConversion = resolver.GetConversion(node);
			if (Compare(expectedConversion, actualConversion, typeof(Conversion)))
				return true;
			Console.WriteLine("Different conversions for '{0}' at {1} in {2}:", node, node.StartLocation, node.GetRegion().FileName);
			Console.WriteLine(" expected: " + expectedConversion);
			Console.WriteLine(" actual:   " + actualConversion);
			return false;
		}
		
		bool CheckExpectedType(Expression node)
		{
			IType expectedExpectedType = resolveAllResolver.GetExpectedType(node);
			IType actualExpectedType = resolver.GetExpectedType(node);
			if (expectedExpectedType.Equals(actualExpectedType))
				return true;
			Console.WriteLine("Different expected types for '{0}' at {1} in {2}:", node, node.StartLocation, node.GetRegion().FileName);
			Console.WriteLine(" expected: " + expectedExpectedType);
			Console.WriteLine(" actual:   " + actualExpectedType);
			return false;
		}
		
		internal static bool IsEqualResolveResult(ResolveResult rr1, ResolveResult rr2)
		{
			if (rr1 == rr2)
				return true;
			if (rr1 == null || rr2 == null)
				return false;
			if (rr1.GetType() != rr2.GetType())
				return false;
			bool eq = true;
			foreach (var property in rr1.GetType().GetProperties()) {
				object val1 = property.GetValue(rr1, null);
				object val2 = property.GetValue(rr2, null);
				eq &= Compare(val1, val2, property.PropertyType);
			}
			foreach (var field in rr1.GetType().GetFields()) {
				object val1 = field.GetValue(rr1);
				object val2 = field.GetValue(rr2);
				eq &= Compare(val1, val2, field.FieldType);
			}
			return eq;
		}
		
		static bool Compare(object val1, object val2, Type type)
		{
			if (val1 == val2)
				return true;
			if (val1 == null || val2 == null)
				return false;
			if (type == typeof(ResolveResult)) {
				return IsEqualResolveResult((ResolveResult)val1, (ResolveResult)val2);
			} else if (type == typeof(IVariable) || type == typeof(IParameter)) {
				return IsEqualVariable((IVariable)val1, (IVariable)val2);
			} else if (type == typeof(MethodListWithDeclaringType)) {
				var l1 = (MethodListWithDeclaringType)val1;
				var l2 = (MethodListWithDeclaringType)val2;
				return object.Equals(l1.DeclaringType, l2.DeclaringType)
					&& Compare(l1, l2, type.BaseType);
			} else if (type.IsArray || type.IsGenericType && (type.GetGenericTypeDefinition() == typeof(List<>) || type.GetGenericTypeDefinition() == typeof(ReadOnlyCollection<>) || type.GetGenericTypeDefinition() == typeof(IList<>) || type.GetGenericTypeDefinition() == typeof(ICollection<>) || type.GetGenericTypeDefinition() == typeof(IEnumerable<>))) {
				Type elementType = type.IsArray ? type.GetElementType() : type.GetGenericArguments()[0];
				object[] arr1 = ((IEnumerable)val1).Cast<object>().ToArray();
				object[] arr2 = ((IEnumerable)val2).Cast<object>().ToArray();
				if (arr1.Length != arr2.Length)
					return false;
				for (int i = 0; i < arr1.Length; i++) {
					if (!Compare(arr1[i], arr2[i], elementType))
						return false;
				}
				return true;
			} else {
				if (object.Equals(val1, val2))
					return true;
				else if (val1 is Conversion && val2 is Conversion && ((Conversion)val1).IsAnonymousFunctionConversion && ((Conversion)val2).IsAnonymousFunctionConversion)
					return true;
				else
					return false;
			}
		}
		
		internal static bool IsEqualResolverState(CSharpResolver r1, CSharpResolver r2)
		{
			if (r1.CheckForOverflow != r2.CheckForOverflow)
				return false;
			if (r1.Compilation != r2.Compilation)
				return false;
			if (!object.Equals(r1.CurrentMember, r2.CurrentMember))
				return false;
			if (!object.Equals(r1.CurrentObjectInitializerType, r2.CurrentObjectInitializerType))
				return false;
			if (!object.Equals(r1.CurrentTypeDefinition, r2.CurrentTypeDefinition))
				return false;
			if (!object.Equals(r1.CurrentUsingScope, r2.CurrentUsingScope))
				return false;
			if (r1.IsWithinLambdaExpression != r2.IsWithinLambdaExpression)
				return false;
			if (r1.LocalVariables.Count() != r2.LocalVariables.Count())
				return false;
			return r1.LocalVariables.Zip(r2.LocalVariables, IsEqualVariable).All(_ => _);
		}
		
		internal static bool IsEqualVariable(IVariable v1, IVariable v2)
		{
			return object.Equals(v1.ConstantValue, v2.ConstantValue)
				&& v1.IsConst == v2.IsConst
				&& v1.Name == v2.Name
				&& v1.Region == v2.Region
				&& object.Equals(v1.Type, v2.Type);
		}
		
		sealed class ResolveAllNavigator : IResolveVisitorNavigator
		{
			public ResolveVisitorNavigationMode Scan(AstNode node)
			{
				return ResolveVisitorNavigationMode.Resolve;
			}
			
			public void Resolved(AstNode node, ResolveResult result)
			{
			}
			
			public void ProcessConversion(Expression expression, ResolveResult result, Conversion conversion, IType targetType)
			{
			}
		}
	}
}
