using InABox.Clients;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace InABox.Core
{
    public static class Expressions
    {
        #region Property Getter Setter Functions

        // Allows setting of object properties via cached expressions

        public static Expression NullPropagatingPropertyOrField(Expression expression, string propName)
        {
            var param = Expression.Parameter(expression.Type);
            var access = Expression.PropertyOrField(param, propName);
            return Expression.Block(new[] { param },
                Expression.Assign(param, expression),
                Expression.Condition(Expression.Equal(param, Expression.Constant(null)),
                    Expression.Default(access.Type),
                    access));
        }

        private static Func<T, TProp>? MakeGetter<T, TProp>(Type objectType, string propname, bool propagateNulls = false)
        {
            try
            {
                var objectParameter = Expression.Parameter(typeof(T), "o");
                Expression param;
                if(typeof(T) != objectType)
                {
                    param = Expression.ConvertChecked(objectParameter, objectType);
                }
                else
                {
                    param = objectParameter;
                }

                var body = param;
                if(propname != "")
                {
                    foreach (var member in propname.Split('.'))
                        body = propagateNulls
                            ? NullPropagatingPropertyOrField(body, member)
                            : Expression.PropertyOrField(body, member);
                }

                if(typeof(TProp) != body.Type)
                {
                    body = Expression.Convert(body, typeof(TProp));
                }

                var lambda = Expression.Lambda<Func<T, TProp>>(body, objectParameter);
                return lambda.Compile();
            }
            catch (Exception e)
            {
                Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace));
                return null;
            }
        }

        public static Func<T, TValue> Getter<T, TValue>(Expression<Func<T, TValue>> expression, bool propagateNulls = false)
        {
            return propagateNulls
                ?  MakeGetter<T, TValue>(typeof(T), CoreUtils.GetFullPropertyName(expression, "."), propagateNulls: true)
                : expression.Compile();
        }

        public static Func<T, object> Getter<T>(string propname, bool propagateNulls = false)
        {
            return MakeGetter<T, object>(typeof(T), propname, propagateNulls: propagateNulls);
        }

        public static Func<object, object> Getter(Type objectType, string propname, bool propagateNulls = false)
        {
            return MakeGetter<object, object>(objectType, propname, propagateNulls: propagateNulls);
        }

        public static Func<T, object> Getter<T>(string propname, string key)
        {
            Func<T, object> result = null;
            try
            {
                var param = Expression.Parameter(typeof(T), "o");

                Expression body = param;
                foreach (var member in propname.Split('.'))
                    body = Expression.PropertyOrField(body, member);

                Expression keyExpr = Expression.Constant(key, typeof(string)); //Parameter(typeof(string));

                // Alternative, note that we could even look for the type of parameters, if there are indexer overloads.
                var indexer = (from p in body.Type.GetDefaultMembers().OfType<PropertyInfo>()
                    // This check is probably useless. You can't overload on return value in C#.
                    where p.PropertyType == typeof(object)
                    let q = p.GetIndexParameters()
                    // Here we can search for the exact overload. Length is the number of "parameters" of the indexer, and then we can check for their type.
                    where q.Length == 1 && q[0].ParameterType == typeof(string)
                    select p).Single();

                var indexExpr = Expression.Property(body, indexer, keyExpr);

                var lambdaGetter = Expression.Lambda<Func<T, object>>(indexExpr, param);
                result = lambdaGetter.Compile();
            }
            catch (Exception e)
            {
                Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace));
            }

            return result;
        }

        public static Func<object, object> Getter(Type objectType, string propname, string key)
        {
            Func<object, object> result = null;
            try
            {
                var objectParameter = Expression.Parameter(typeof(object), "o");
                var param = Expression.ConvertChecked(objectParameter, objectType);

                Expression body = param;
                foreach (var member in propname.Split('.'))
                    body = Expression.PropertyOrField(body, member);

                Expression keyExpr = Expression.Constant(key, typeof(string)); //Parameter(typeof(string));

                // Alternative, note that we could even look for the type of parameters, if there are indexer overloads.
                var indexer = (from p in body.Type.GetDefaultMembers().OfType<PropertyInfo>()
                    // This check is probably useless. You can't overload on return value in C#.
                    where p.PropertyType == typeof(object)
                    let q = p.GetIndexParameters()
                    // Here we can search for the exact overload. Length is the number of "parameters" of the indexer, and then we can check for their type.
                    where q.Length == 1 && q[0].ParameterType == typeof(string)
                    select p).Single();

                var indexExpr = Expression.Property(body, indexer, keyExpr);

                var lambdaGetter = Expression.Lambda<Func<object, object>>(indexExpr, objectParameter);
                result = lambdaGetter.Compile();
            }
            catch (Exception e)
            {
                Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace));
            }

            return result;
        }

        public static bool IsWriteable(Expression expression)
        {
            switch (expression.NodeType)
            {
                case ExpressionType.Index:
                    return expression is IndexExpression index && index.Indexer is PropertyInfo indexer && indexer.CanWrite;
                case ExpressionType.MemberAccess:
                    if(expression is MemberExpression me)
                    {
                        if (me.Member is PropertyInfo prop)
                            return prop.CanWrite;
                        else if (me.Member is FieldInfo field)
                            return !(field.IsInitOnly || field.IsLiteral);
                    }
                    return false;
                case ExpressionType.Parameter:
                    return true;
            }
            return false;
        }

        public static Action<T, object?> Setter<T>(string propname)
        {
            var param = Expression.Parameter(typeof(T), "o");

            Expression body = param;
            foreach (var member in propname.Split('.'))
                body = Expression.PropertyOrField(body, member);

            var valueParameter = Expression.Parameter(typeof(object), "v");
            var value = Expression.ConvertChecked(valueParameter, body.Type);

            Expression expression;
            if (IsWriteable(body))
            {
                try
                {
                    expression = Expression.Assign(body, value);
                }
                catch
                {
                    expression = Expression.Empty();
                }
            }
            else
            {
                expression = Expression.Empty();
            }

            var lambda = Expression.Lambda<Action<T, object?>>(expression, param, valueParameter);
            var compiled = lambda.Compile();
            return compiled;
        }

        public static Action<object, object?> Setter(Type objectType, string propname, Type? type = null)
        {
            Action<object, object?>? compiled = null;
            try
            {
                var objectParameter = Expression.Parameter(typeof(object), "o");
                var param = Expression.ConvertChecked(objectParameter, objectType);

                Expression body = param;
                foreach (var member in propname.Split('.'))
                    body = Expression.PropertyOrField(body, member);

                var valueParameter = Expression.Parameter(typeof(object), "v");
                UnaryExpression value;
                if (type != null)
                {
                    var val1 = Expression.Convert(valueParameter, type);
                    value = Expression.ConvertChecked(val1, body.Type);
                }
                else
                {
                    value = Expression.ConvertChecked(valueParameter, body.Type);
                }

                Expression expression;
                if (IsWriteable(body))
                {
                    try
                    {
                        expression = Expression.Assign(body, value);
                    }
                    catch
                    {
                        expression = Expression.Empty();
                    }
                }
                else
                {
                    expression = Expression.Empty();
                }

                var lambda = Expression.Lambda<Action<object, object?>>(expression, objectParameter, valueParameter);
                compiled = lambda.Compile();
            }
            catch (Exception e)
            {
                Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace));
            }

            return compiled;
        }

        public static Action<T, object?> Setter<T>(string propname, string key)
        {
            Action<T, object?> result = null;
            try
            {
                var param = Expression.Parameter(typeof(T), "o");

                Expression body = param;
                foreach (var member in propname.Split('.'))
                    body = Expression.PropertyOrField(body, member);

                Expression keyExpr = Expression.Constant(key, typeof(string));

                var valueExpr = Expression.Parameter(typeof(object));

                // Alternative, note that we could even look for the type of parameters, if there are indexer overloads.
                var indexer = (from p in body.Type.GetDefaultMembers().OfType<PropertyInfo>()
                    // This check is probably useless. You can't overload on return value in C#.
                    where p.PropertyType == typeof(object)
                    let q = p.GetIndexParameters()
                    // Here we can search for the exact overload. Length is the number of "parameters" of the indexer, and then we can check for their type.
                    where q.Length == 1 && q[0].ParameterType == typeof(string)
                    select p).Single();

                var indexExpr = Expression.Property(body, indexer, keyExpr);

                var assign = Expression.Assign(indexExpr, valueExpr);

                var lambdaSetter = Expression.Lambda<Action<T, object?>>(assign, param, valueExpr);
                result = lambdaSetter.Compile();
            }
            catch (Exception e)
            {
                Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace));
            }

            return result;
        }

        public static Action<object, object?> Setter(Type objectType, string propname, string key)
        {
            Action<object, object?> result = null;
            try
            {
                var objectParameter = Expression.Parameter(typeof(object), "o");
                var param = Expression.ConvertChecked(objectParameter, objectType);

                Expression body = param;
                foreach (var member in propname.Split('.'))
                    body = Expression.PropertyOrField(body, member);

                Expression keyExpr = Expression.Constant(key, typeof(string));

                var valueExpr = Expression.Parameter(typeof(object));

                // Alternative, note that we could even look for the type of parameters, if there are indexer overloads.
                var indexer = (from p in body.Type.GetDefaultMembers().OfType<PropertyInfo>()
                    // This check is probably useless. You can't overload on return value in C#.
                    where p.PropertyType == typeof(object)
                    let q = p.GetIndexParameters()
                    // Here we can search for the exact overload. Length is the number of "parameters" of the indexer, and then we can check for their type.
                    where q.Length == 1 && q[0].ParameterType == typeof(string)
                    select p).Single();

                var indexExpr = Expression.Property(body, indexer, keyExpr);

                var assign = Expression.Assign(indexExpr, valueExpr);

                var lambdaSetter = Expression.Lambda<Action<object, object?>>(assign, objectParameter, valueExpr);
                result = lambdaSetter.Compile();
            }
            catch (Exception e)
            {
                Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace));
            }

            return result;
        }

        #endregion
    }
}