//
// MemberInvocationReference.cs
//
// Authors:
//	Marek Safar  <marek.safar@gmail.com>
//
// Copyright (C) 2014 Xamarin Inc (http://www.xamarin.com)
//
// 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.Reflection;
using System.Globalization;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using System.Text;

namespace Microsoft.Build.BuildEngine
{
	class MemberInvocationReference : IReference
	{
		Type type;
		readonly string name;

		static readonly char[] ArgumentTrimChars = new char[] { '\"', '\'', '`' };
		static readonly object ConversionFailed = new object ();

		public MemberInvocationReference (Type type, string name)
		{
			this.type = type;
			this.name = name;
		}

		public List<string> Arguments { get; set; }

		public IReference Instance { get; set; }

		public string ConvertToString (Project project, ExpressionOptions options)
		{
			return ConvertResult (Invoke (project, options));
		}

		object Invoke (Project project, ExpressionOptions options)
		{
			var flags = BindingFlags.IgnoreCase | BindingFlags.Public;
			object target;
			string member_name = name;

			if (Instance == null) {
				target = null;
				if (string.Equals (member_name, "new", StringComparison.OrdinalIgnoreCase)) {
					member_name = ConstructorInfo.ConstructorName;
					flags |= BindingFlags.CreateInstance | BindingFlags.Instance;
				} else {
					flags |= BindingFlags.Static;
				}
			} else {
				var mir = Instance as MemberInvocationReference;
				if (mir != null) {
					target = mir.Invoke (project, options);
					if (target == null) {
						throw new NotImplementedException ("Instance method on null value");
					}

					type = target.GetType ();
				} else {
					target = Instance.ConvertToString (project, options);
					type = typeof (string);
				}

				flags |= BindingFlags.Instance;
			}

			object[] args;
			if (Arguments == null) {
				if ((flags & BindingFlags.CreateInstance) == 0)
					flags |= BindingFlags.GetProperty;
				args = null;
			} else {
				if ((flags & BindingFlags.CreateInstance) == 0)
					flags |= BindingFlags.InvokeMethod;
				ExpandArguments (project, options);
				args = PrepareMethodArguments (member_name, flags);
				if (args == null)
					throw new InvalidProjectFileException (string.Format ("Method '{0}({1})' arguments cannot be evaluated'", name, string.Join (", ", Arguments.ToArray ())));
			}

			object value;
			try {
				value = type.InvokeMember (member_name, flags, null, target, args, CultureInfo.InvariantCulture);
			} catch (MissingMethodException) {
				//
				// It can be field/constant instead of a property
				//
				if (args == null && Instance == null) {
					flags &= ~BindingFlags.GetProperty;
					flags |= BindingFlags.GetField;
					value = type.InvokeMember (member_name, flags, null, null, null, CultureInfo.InvariantCulture);
				} else {
					throw;
				}
			}

			return value;
		}

		static string TrimFirstAndLast(string unTrimmed)
		{
			if (unTrimmed.Length > 1 && Array.IndexOf (ArgumentTrimChars, unTrimmed [0]) != -1 && Array.IndexOf (ArgumentTrimChars, unTrimmed [unTrimmed.Length - 1]) != -1) {
				return unTrimmed.Substring (1, unTrimmed.Length - 2);
			}
			return unTrimmed;
		}

		void ExpandArguments (Project project, ExpressionOptions options)
		{
			for (int i = 0; i < Arguments.Count; ++i) {
				string arg = Arguments [i].Trim ();
				if (string.Equals (arg, "null", StringComparison.OrdinalIgnoreCase)) {
					arg = null;
				} else {
					arg = Expression.ParseAs<string> (arg, ParseOptions.None,
						project, options);

					arg = TrimFirstAndLast (arg);
				}

				Arguments [i] = arg;
			}
		}

		object[] PrepareMethodArguments (string name, BindingFlags flags)
		{
			var candidates = type.GetMember (name, MemberTypes.Method | MemberTypes.Constructor, flags);
			object[] args = null;
			ParameterInfo[] best = null;
			foreach (MethodBase candidate in candidates) {
				var parameters = candidate.GetParameters ();
				if (parameters.Length != Arguments.Count)
					continue;

				if (parameters.Length == 0)
					return new object [0];

				object[] cand_args = null;
				for (int i = 0; i < parameters.Length; ++i) {
					var target = ConvertArgument (Arguments [i], parameters [i]);
					if (target == ConversionFailed) {
						cand_args = null;
						break;
					}

					if (cand_args == null)
						cand_args = new object[parameters.Length];

					cand_args [i] = target;
				}

				if (cand_args == null)
					continue;

				if (args == null) {
					args = cand_args;
					best = parameters;
					continue;
				}

				if (BetterCandidate (best, parameters) > 1) {
					args = cand_args;
					best = parameters;
				}
			}

			return args;
		}

		static object ConvertArgument (object value, ParameterInfo target)
		{
			var ptype = target.ParameterType;
			if (ptype.IsEnum) {
				var s = value as string;
				if (s != null)
					return ConvertToEnum (s, ptype);
			} else if (ptype == typeof (char[])) {
				var s = value as string;
				if (s != null)
					return s.ToCharArray ();
			}

			try {
				return Convert.ChangeType (value, ptype, CultureInfo.InvariantCulture);
			} catch {
				return ConversionFailed;
			}
		}

		static object ConvertToEnum (string s, Type type)
		{
			var dot = s.IndexOf ('.');
			if (dot < 0)
				return ConversionFailed;

			var fn = type.FullName + ".";
			if (s.StartsWith (fn, StringComparison.Ordinal)) {
				s = s.Substring (fn.Length);
			} else if (s.StartsWith (type.Name, StringComparison.Ordinal) && s [type.Name.Length] == '.') {
				s = s.Substring (type.Name.Length + 1);
			}

			try {
				return Enum.Parse (type, s);
			} catch {
				return ConversionFailed;
			}
		}

		static string ConvertResult (object value)
		{
			if (value is string)
				return (string)value;

			var e = value as IEnumerable;
			if (e != null) {
				var sb = new StringBuilder ();
				foreach (var entry in e) {
					if (sb.Length > 0)
						sb.Append (";");

					sb.Append (ConvertResult (entry));
				}

				return sb.ToString ();
			}

			return value == null ? "" : value.ToString ();
		}

		//
		// Returns better candidate for untyped string values. We can really do only
		// preference for string over any other types
		//
		// 1: a is better
		// 2: b is better
		// 0: neither is better
		//
		static int BetterCandidate (ParameterInfo[] a, ParameterInfo[] b)
		{
			int res = 0;
			for (int i = 0; i < a.Length; ++i) {
				var atype = a [i].ParameterType;
				var btype = b [i].ParameterType;

				if (atype == typeof (string) && btype != atype) {
					if (res < 2)
						res = 1;
					continue;
				}

				if (btype == typeof (string) && btype != atype) {
					if (res != 1)
						res = 2;

					continue;
				}
			}

			return res;
		}

		public ITaskItem[] ConvertToITaskItemArray (Project project, ExpressionOptions options)
		{
			var items = new ITaskItem[1];
			items[0] = new TaskItem (ConvertToString (project, options));

			return items;
		}
	}
}