﻿// Any comments, input: @KevinDockx
// Any issues, requests: https://github.com/KevinDockx/JsonPatch
//
// Enjoy :-)

using Newtonsoft.Json;
using System;
using System.Globalization;
using System.Linq.Expressions;

namespace Marvin.JsonPatch.Helpers
{
    /// <summary>
    /// Expression helpers. These can be used when implementing custom ObjectAdapters.
    /// </summary>
    public static class ExpressionHelpers
    {
        /// <summary>
        /// Returns the path from a given expression.  Takes JsonProperty into account.
        /// </summary>
        /// <typeparam name="T">Class type</typeparam>
        /// <typeparam name="TProp">Property type on class</typeparam>
        /// <param name="expr">Expression the path must be returned from</param>
        /// <returns>Property path</returns>
        public static string GetPath<T, TProp>(Expression<Func<T, TProp>> expr, CaseTransformType caseTransformType) where T : class
        {        
            return "/" + GetPath(expr.Body, caseTransformType, true);
        }
 
        private static string GetPath(Expression expr, CaseTransformType caseTransformType, bool firstTime)
        {
            switch (expr.NodeType)
            {
                case ExpressionType.ArrayIndex:
                    var binaryExpression = (BinaryExpression)expr;

                    if (ContinueWithSubPath(binaryExpression.Left.NodeType, false))
                    {
                        var leftFromBinaryExpression = GetPath(binaryExpression.Left, caseTransformType, false);
                        return leftFromBinaryExpression + "/" + CaseTransform(binaryExpression.Right.ToString(), caseTransformType);
                    }
                    else
                    {
                        return CaseTransform(binaryExpression.Right.ToString(), caseTransformType);
                    }

                case ExpressionType.Call:
                    var methodCallExpression = (MethodCallExpression)expr;

                    if (ContinueWithSubPath(methodCallExpression.Object.NodeType, false))
                    {
                        var leftFromMemberCallExpression = GetPath(methodCallExpression.Object, caseTransformType, false);
                        return leftFromMemberCallExpression + "/" +
                            CaseTransform(GetIndexerInvocation(methodCallExpression.Arguments[0]), caseTransformType);
                    }
                    else
                    {
                        return CaseTransform(GetIndexerInvocation(methodCallExpression.Arguments[0]), caseTransformType);
                    }

                case ExpressionType.Convert:
                    return GetPath(((UnaryExpression)expr).Operand, caseTransformType, false);

                case ExpressionType.MemberAccess:
                    var memberExpression = expr as MemberExpression;

                    if (ContinueWithSubPath(memberExpression.Expression.NodeType, false))
                    {
                        var left = GetPath(memberExpression.Expression, caseTransformType, false);

                        // if there's a JsonProperty attribute, we must return the PropertyName
                        // from the attribute rather than the member name 

                        var jsonPropertyAttribute =
                            memberExpression.Member.GetCustomAttributes(
                            typeof(JsonPropertyAttribute), false);

                        if (jsonPropertyAttribute.Length > 0)
                        {
                            // get value
                            var castedAttribrute = jsonPropertyAttribute[0] as JsonPropertyAttribute;
                            return left + "/" + CaseTransform(castedAttribrute.PropertyName, caseTransformType);
                        }

                        return left + "/" + CaseTransform(memberExpression.Member.Name, caseTransformType);
                    }
                    else
                    {
                        // Same here: if there's a JsonProperty attribute, we must return the PropertyName
                        // from the attribute rather than the member name 

                        var jsonPropertyAttribute =
                            memberExpression.Member.GetCustomAttributes(
                            typeof(JsonPropertyAttribute), false);

                        if (jsonPropertyAttribute.Length > 0)
                        {
                            // get value
                            var castedAttribrute = jsonPropertyAttribute[0] as JsonPropertyAttribute;
                            return CaseTransform(castedAttribrute.PropertyName, caseTransformType);
                        }

                        return CaseTransform(memberExpression.Member.Name, caseTransformType);
                    }

                case ExpressionType.Parameter:
                    // Fits "x => x" (the whole document which is "" as JSON pointer)
                    return firstTime ? "" : null;

                default:
                    return "";
            }
        }

        private static bool ContinueWithSubPath(ExpressionType expressionType, bool firstTime)
        {
            if (firstTime)
            {
                return (expressionType == ExpressionType.ArrayIndex
                       || expressionType == ExpressionType.Call
                       || expressionType == ExpressionType.Convert
                       || expressionType == ExpressionType.MemberAccess
                       || expressionType == ExpressionType.Parameter);
            }
            else
            {
                return (expressionType == ExpressionType.ArrayIndex
                    || expressionType == ExpressionType.Call
                    || expressionType == ExpressionType.Convert
                    || expressionType == ExpressionType.MemberAccess);
            }
        }

        private static string GetIndexerInvocation(Expression expression)
        {
            var converted = Expression.Convert(expression, typeof(object));
            var fakeParameter = Expression.Parameter(typeof(object), null);
            var lambda = Expression.Lambda<Func<object, object>>(converted, fakeParameter);
            Func<object, object> func;

            func = lambda.Compile();

            return Convert.ToString(func(null), CultureInfo.InvariantCulture);
        }

        public static string CaseTransform(string propertyName, CaseTransformType type)
        {

            if (string.IsNullOrWhiteSpace(propertyName)) return propertyName;

            string result = string.Empty;

            switch (type)
            {
                case CaseTransformType.CamelCase:
                    result = Char.ToLowerInvariant(propertyName[0]) + propertyName.Substring(1);
                    break;
                case CaseTransformType.LowerCase:
                    result = propertyName.ToLowerInvariant();
                    break;
                case CaseTransformType.UpperCase:
                    result = propertyName.ToUpperInvariant();
                    break;
                case CaseTransformType.OriginalCase:
                    result = propertyName;
                    break;
                default:
                    throw new NotImplementedException();
            }

            return result;
        }
    }
}
