// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using HandlebarsDotNet;
using JetBrains.Annotations;

namespace Microsoft.DotNet.SwaggerGenerator
{
    [AttributeUsage(AttributeTargets.Method)]
    [MeansImplicitUse(ImplicitUseKindFlags.Access)]
    public class HelperMethodAttribute : Attribute
    {
    }

    [AttributeUsage(AttributeTargets.Method)]
    [MeansImplicitUse(ImplicitUseKindFlags.Access)]
    public class BlockHelperMethodAttribute : Attribute
    {
    }

    internal static class HelperFactory
    {
        internal static void RegisterAllForType(IHandlebars hb, Type type, object instance)
        {
            var helpers = CreateHelpersForType(type, instance);

            foreach (var (name, helper) in helpers)
            {
                hb.RegisterHelper(name, helper);
            }

            var blockHelpers = CreateBlockHelpersForType(type, instance);

            foreach (var (name, helper) in blockHelpers)
            {
                hb.RegisterHelper(name, helper);
            }
        }

        private static List<(string name, HandlebarsBlockHelper helper)> CreateBlockHelpersForType(Type type, object instance)
        {
            var helpers = new List<(string name, HandlebarsBlockHelper helper)>();
            foreach (var method in GetAllMethods(type))
            {
                if (method.GetCustomAttribute<BlockHelperMethodAttribute>() == null)
                {
                    continue;
                }

                if (!method.IsPublic)
                {
                    continue;
                }

                if (instance == null && !method.IsStatic)
                {
                    continue;
                }

                helpers.Add(CreateBlockHelperForMethod(method, instance));
            }

            return helpers;
        }

        private static List<(string name, HandlebarsHelper helper)> CreateHelpersForType(Type type, object instance)
        {
            var helpers = new List<(string name, HandlebarsHelper helper)>();
            foreach (var method in GetAllMethods(type))
            {
                if (method.GetCustomAttribute<HelperMethodAttribute>() == null)
                {
                    continue;
                }

                if (!method.IsPublic)
                {
                    continue;
                }

                if (instance == null && !method.IsStatic)
                {
                    continue;
                }

                helpers.Add(CreateHelperForMethod(method, instance));
            }

            return helpers;
        }

        private static IEnumerable<MethodInfo> GetAllMethods(Type type)
        {
            while (type != null)
            {
                foreach (var method in type.GetRuntimeMethods())
                {
                    yield return method;
                }
                type = type.BaseType;
            }
        }

        private static (string name, HandlebarsBlockHelper helper) CreateBlockHelperForMethod(MethodInfo method, object instance)
        {
            var name = Helpers.CamelCase(method.Name.AsSpan());
            var fn = CreateBlockHelperFunctionForMethod(method, instance);
            return (name, fn);
        }

        private static (string name, HandlebarsHelper helper) CreateHelperForMethod(MethodInfo method, object instance)
        {
            var name = Helpers.CamelCase(method.Name.AsSpan());
            var fn = CreateHelperFunctionForMethod(method, instance);
            return (name, fn);
        }

        private static HandlebarsBlockHelper CreateBlockHelperFunctionForMethod(MethodInfo method, object instance)
        {
            var outputParameter = Expression.Parameter(typeof(TextWriter), "output");
            var optionsParameter = Expression.Parameter(typeof(HelperOptions), "options");
            var contextParameter = Expression.Parameter(typeof(object), "context");
            var argumentsParameter = Expression.Parameter(typeof(object[]), "arguments");

            var parameterExpressions = GetParameterExpressions(method, argumentsParameter, new List<ProvidedParameter>
            {
                new ProvidedParameter("context", typeof(object), contextParameter),
                new ProvidedParameter("output", typeof(TextWriter), outputParameter),
                new ProvidedParameter("template", typeof(Action<TextWriter, object>), Expression.MakeMemberAccess(optionsParameter, typeof(HelperOptions).GetProperty("Template"))),
                new ProvidedParameter("inverse", typeof(Action<TextWriter, object>), Expression.MakeMemberAccess(optionsParameter, typeof(HelperOptions).GetProperty("Inverse"))),
            });

            Expression invokeExpression;
            if (!method.IsStatic)
            {
                invokeExpression = Expression.Call(Expression.Constant(instance), method, parameterExpressions);
            }
            else
            {
                invokeExpression = Expression.Call(method, parameterExpressions);
            }

            Expression body;
            if (method.ReturnType == typeof(void))
            {
                body = invokeExpression;
            }
            else
            {
                var writerOutput = ConvertResultExpression(method.ReturnType, invokeExpression);
                body = Expression.Call(HandlebarsExtensionsWriteSafeString, outputParameter, writerOutput);
            }

            var result = Expression.Lambda<HandlebarsBlockHelper>(
                body,
                outputParameter,
                optionsParameter,
                contextParameter,
                argumentsParameter);
            Debug.WriteLine("Compiling Expression: " + result);
            var function = result.Compile();
            return (output, options, context, parameters) => { function(output, options, context, parameters); };
        }

        private static HandlebarsHelper CreateHelperFunctionForMethod(MethodInfo method, object instance)
        {
            var outputParameter = Expression.Parameter(typeof(TextWriter), "output");
            var contextParameter = Expression.Parameter(typeof(object), "context");
            var argumentsParameter = Expression.Parameter(typeof(object[]), "arguments");

            var parameterExpressions = GetParameterExpressions(method, argumentsParameter, new List<ProvidedParameter>
            {
                new ProvidedParameter("context", typeof(object), contextParameter),
                new ProvidedParameter("output", typeof(TextWriter), outputParameter),
            });

            Expression invokeExpression;
            if (!method.IsStatic)
            {
                invokeExpression = Expression.Call(Expression.Constant(instance), method, parameterExpressions);
            }
            else
            {
                invokeExpression = Expression.Call(method, parameterExpressions);
            }
            var writerOutput = ConvertResultExpression(method.ReturnType, invokeExpression);
            var body = Expression.Call(HandlebarsExtensionsWriteSafeString, outputParameter, writerOutput);

            var result = Expression.Lambda<HandlebarsHelper>(
                body,
                outputParameter,
                contextParameter,
                argumentsParameter);
            Debug.WriteLine("Compiling Expression: " + result.ToString());
            var function = result.Compile();
            return (output, context, parameters) => { function(output, context, parameters); };
        }

        private static MethodInfo ObjectToString = typeof(object).GetMethod("ToString");

        private static MethodInfo HandlebarsExtensionsWriteSafeString = typeof(HandlebarsExtensions).GetMethod(
            "WriteSafeString",
            new[] {typeof(TextWriter), typeof(string)});

        private static MethodInfo EnumerableSkip(Type member) =>
            typeof(Enumerable).GetMethod("Skip").MakeGenericMethod(member);

        private static MethodInfo EnumerableToArray(Type member) =>
            typeof(Enumerable).GetMethod("ToArray").MakeGenericMethod(member);

        private static MethodInfo EnumerableSelect(Type input, Type output) =>
            typeof(Enumerable).GetMethods()
                .Single(
                    m => m.Name == "Select" &&
                         m.GetParameters().Length == 2 &&
                         m.GetParameters()[1].ParameterType.GetGenericTypeDefinition() == typeof(Func<,>))
                .MakeGenericMethod(input, output);

        private static MethodInfo ConvertChangeType = typeof(Convert).GetMethod(
            "ChangeType",
            new[] {typeof(object), typeof(Type)});

        private static MethodInfo HandlebarsUtilsIsTruthyOrNonEmpty = typeof(HandlebarsUtils).GetMethod("IsTruthyOrNonEmpty");

        private class ProvidedParameter
        {
            public ProvidedParameter(string name, Type type, Expression value)
            {
                Name = name;
                Type = type;
                Value = value;
            }

            public string Name { get; }
            public Type Type { get; }
            public Expression Value { get; }
        }

        private static IEnumerable<Expression> GetParameterExpressions(MethodInfo method, ParameterExpression argumentsParameter, List<ProvidedParameter> providedParameters)
        {
            var parameters = method.GetParameters();
            var consumedInputCount = 0;

            for (int i = 0; i < parameters.Length; i++)
            {
                var parameter = parameters[i];
                var providedParameter = providedParameters.FirstOrDefault(
                    p => p.Type == parameter.ParameterType && p.Name == parameter.Name);
                if (providedParameter != null)
                {
                    yield return providedParameter.Value;
                    continue;
                }

                yield return GetExpressionForParameter(parameter, consumedInputCount, argumentsParameter);
                consumedInputCount++;
            }
        }

        private static readonly Type UndefinedBindingResultType =
            typeof(Handlebars).Assembly.GetType("HandlebarsDotNet.Compiler.UndefinedBindingResult");

        private static Expression CoerceObjectExpression(Type output, Expression input)
        {
            if (output == typeof(bool))
            {
                return Expression.Call(HandlebarsUtilsIsTruthyOrNonEmpty, input);
            }

            if (output.IsPrimitive)
            {
                return Expression.Convert(Expression.Call(ConvertChangeType, input, Expression.Constant(output)), output);
            }

            if (!output.IsValueType ||
                (output.IsConstructedGenericType && output.GetGenericTypeDefinition() == typeof(Nullable<>)))
            {
                input = Expression.Condition(
                    Expression.TypeIs(input, UndefinedBindingResultType),
                    Expression.Constant(null, typeof(object)),
                    input);
            }

            return Expression.Convert(input, output);
        }

        private static Expression ConvertResultExpression(Type type, Expression input)
        {
            if (type == typeof(bool))
            {
                return Expression.Condition(input, Expression.Constant("true"), Expression.Constant(""));
            }

            return Expression.Call(input, ObjectToString);
        }

        private static Expression GetExpressionForParameter(
            ParameterInfo parameter,
            int index,
            ParameterExpression argumentsParameter)
        {
            var parameterType = parameter.ParameterType;
            if (parameter.ParameterType.IsArray && parameter.GetCustomAttribute<ParamArrayAttribute>() != null)
            {
                return GetExpressionForParamArrayParameter(parameterType, index, argumentsParameter);
            }

            var element = Expression.ArrayIndex(argumentsParameter, Expression.Constant(index));
            return CoerceObjectExpression(parameterType, element);
        }

        private static Expression GetExpressionForParamArrayParameter(Type parameterType, int index, ParameterExpression argumentsParameter)
        {
            Expression result = argumentsParameter;
            if (index != 0)
            {
                result = Expression.Call(EnumerableSkip(typeof(object)), result, Expression.Constant(index));
            }

            var elementType = parameterType.GetElementType();
            var selectParam = Expression.Parameter(typeof(object), "o");

            result = Expression.Call(
                EnumerableToArray(elementType),
                Expression.Call(
                    EnumerableSelect(typeof(object), elementType),
                    result,
                    Expression.Lambda(CoerceObjectExpression(elementType, selectParam), selectParam)));

            return result;
        }
    }
}
