using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace CefBridgeDataExchange
{
    public class DataExchangeDispatcherBuilder
    {
        private readonly Dictionary<string, Dictionary<string, ServiceActionBind>> _controllerDictionary;
        private readonly IDataExchangeDispatcher _dispatcher;

        public DataExchangeDispatcherBuilder()
        {
            _controllerDictionary = new Dictionary<string, Dictionary<string, ServiceActionBind>>();
            _dispatcher = new DataExchangeDispatcher(_controllerDictionary);
        }

        public void RegisterService<TInterface>(TInterface instance, bool overrideExist = false)
        {
            RegisterService(typeof(TInterface).Name, instance, overrideExist);
        }

        public void RegisterService<TInterface>(string name, TInterface instance, bool overrideExist = false)
        {
            if (null == instance) throw new ArgumentNullException(nameof(instance));

            if (_controllerDictionary.ContainsKey(name) && false == overrideExist)
                throw new ArgumentException("Service name already existed");

            var serviceType = typeof(TInterface);

            var methodInfos = serviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance).Where(p => p.GetParameters().Length < 2).ToArray();
            if (methodInfos.Select(p => p.Name).Distinct().Count() != methodInfos.Length) throw new NotSupportedException("Overload is not supported");

            var jsonDeserializeObject = typeof(JsonConvert).GetMethod(nameof(JsonConvert.DeserializeObject), new[] { typeof(string), typeof(Type) })
                                        ?? throw new MissingMethodException(nameof(JsonConvert), nameof(JsonConvert.DeserializeObject));

            _controllerDictionary[name] = methodInfos.ToDictionary(p => p.Name, methodInfo =>
            {
                var parameterInfos = methodInfo.GetParameters();
                if (1 < parameterInfos.Length) throw new NotSupportedException($"Maximum 1 param supported:{instance.GetType().FullName}.{methodInfo.Name}");

                var pJson = Expression.Parameter(typeof(string));
                var pInstance = Expression.Parameter(typeof(object));

                var unboxInstance = Expression.Convert(pInstance, serviceType);

                Func<string, object, object> deg;

                if (0 == parameterInfos.Length)
                {
                    var invoke = Expression.Call(unboxInstance, methodInfo);
                    if (typeof(void) == methodInfo.ReturnType)
                    {
                        var lam = Expression.Lambda<Action<string, object>>(invoke, pJson, pInstance);
                        var com = lam.Compile();
                        deg = (s, o) =>
                        {
                            com(s, o);
                            return null;
                        };
                    }
                    else
                    {
                        var boxedResult = Expression.Convert(invoke, typeof(object));
                        var lam = Expression.Lambda<Func<string, object, object>>(boxedResult, pJson, pInstance);
                        deg = lam.Compile();
                    }
                }
                else
                {
                    var paramType = parameterInfos[0].ParameterType;
                    var jsonObject = Expression.Call(jsonDeserializeObject, pJson, Expression.Constant(paramType));
                    var jsonObjectCast = Expression.Convert(jsonObject, paramType);

                    var invoke = Expression.Call(unboxInstance, methodInfo, jsonObjectCast);

                    if (typeof(void) == methodInfo.ReturnType)
                    {
                        var lam = Expression.Lambda<Action<string, object>>(invoke, pJson, pInstance);
                        var com = lam.Compile();
                        deg = (s, o) =>
                        {
                            com(s, o);
                            return null;
                        };
                    }
                    else
                    {
                        var boxedResult = Expression.Convert(invoke, typeof(object));
                        var lam = Expression.Lambda<Func<string, object, object>>(boxedResult, pJson, pInstance);
                        deg = lam.Compile();
                    }
                }

                return new ServiceActionBind(instance, deg);
            });
        }

        public object Dispatcher => _dispatcher;
    }
}