#region Copyright & License Information
/*
 * Copyright (c) The OpenRA Developers and Contributors
 * This file is part of OpenRA, which is free software. It is made
 * available to you under the terms of the GNU General Public License
 * as published by the Free Software Foundation, either version 3 of
 * the License, or (at your option) any later version. For more
 * information, see COPYING.
 */
#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Eluant;
using OpenRA.Traits;

namespace OpenRA.Scripting
{
	public class ScriptMemberWrapper
	{
		readonly ScriptContext context;
		public readonly object Target;
		public readonly MemberInfo Member;

		public readonly bool IsMethod;
		public readonly bool IsGetProperty;
		public readonly bool IsSetProperty;

		public ScriptMemberWrapper(ScriptContext context, object target, MemberInfo mi)
		{
			this.context = context;
			Target = target;
			Member = mi;

			var property = mi as PropertyInfo;
			if (property != null)
			{
				IsGetProperty = property.GetGetMethod() != null;
				IsSetProperty = property.GetSetMethod() != null;
			}
			else
				IsMethod = true;
		}

		LuaValue Invoke(LuaVararg args)
		{
			object[] clrArgs = null;
			try
			{
				if (!IsMethod)
					throw new LuaException($"Trying to invoke a {nameof(ScriptMemberWrapper)} that isn't a method!");

				var mi = (MethodInfo)Member;
				var pi = mi.GetParameters();

				clrArgs = new object[pi.Length];

				var argCount = args.Count;
				for (var i = 0; i < pi.Length; i++)
				{
					if (i >= argCount)
					{
						if (!pi[i].IsOptional)
							throw new LuaException($"Argument '{pi[i].LuaDocString()}' of '{Member.LuaDocString()}' is not optional.");

						clrArgs[i] = pi[i].DefaultValue;
						continue;
					}

					if (!args[i].TryGetClrValue(pi[i].ParameterType, out clrArgs[i]))
						throw new LuaException($"Unable to convert parameter {i} to {pi[i].ParameterType.Name}");
				}

				return mi.Invoke(Target, clrArgs).ToLuaValue(context);
			}
			finally
			{
				// Clean up all the Lua arguments that were given to us.
				foreach (var arg in args)
					arg.Dispose();
				args.Dispose();

				// If we created any arrays of LuaValues to pass around, we need to dispose those too.
				if (clrArgs != null)
				{
					foreach (var arg in clrArgs)
					{
						if (arg is not LuaValue[] table)
							continue;

						foreach (var value in table)
							value.Dispose();
					}
				}
			}
		}

		public LuaValue Get(LuaRuntime runtime)
		{
			if (IsMethod)
				return runtime.CreateFunctionFromDelegate((Func<LuaVararg, LuaValue>)Invoke);

			if (IsGetProperty)
				return ((PropertyInfo)Member).GetValue(Target, null).ToLuaValue(context);

			throw new LuaException($"The property '{Member.Name}' is write-only");
		}

		public void Set(LuaValue value)
		{
			if (IsSetProperty)
			{
				var pi = (PropertyInfo)Member;
				if (!value.TryGetClrValue(pi.PropertyType, out var clrValue))
					throw new LuaException($"Unable to convert '{value.WrappedClrType().Name}' to CLR type '{pi.PropertyType}'");

				pi.SetValue(Target, clrValue, null);
			}
			else
				throw new LuaException($"The property '{Member.Name}' is read-only");
		}

		public static IEnumerable<MemberInfo> WrappableMembers(Type t)
		{
			// Only expose defined public non-static methods that were explicitly declared by the author
			const BindingFlags Flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly;
			foreach (var mi in t.GetMembers(Flags))
			{
				// Properties are always wrappable
				if (mi is PropertyInfo)
					yield return mi;

				// Methods are allowed if they aren't generic, and aren't generated by the compiler
				var method = mi as MethodInfo;
				if (method != null && !method.IsGenericMethodDefinition && !method.IsSpecialName)
					yield return mi;

				// Fields aren't allowed
			}
		}

		public static string[] RequiredTraitNames(Type t)
		{
			// Returns the inner types of all the Requires<T> interfaces on this type
			var types = t.GetInterfaces()
				.Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(Requires<>));

			// Remove the namespace and the trailing "Info"
			return types.SelectMany(i => i.GetGenericArguments())
				.Select(g => g.Name.Split('.', StringSplitOptions.RemoveEmptyEntries).LastOrDefault())
				.Select(s => s.EndsWith("Info", StringComparison.Ordinal) ? s.Remove(s.Length - 4, 4) : s)
				.ToArray();
		}
	}
}
