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

using System.Collections.Generic;
using System.Diagnostics;
using System.Dynamic.Utils;
using System.Globalization;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using AstUtils = System.Linq.Expressions.Utils;

namespace System.Linq.Expressions.Interpreter
{
    [DebuggerDisplay("{DebugView,nq}")]
    public partial class LightLambda
    {
        private readonly IStrongBox[]? _closure;
        private readonly Interpreter _interpreter;
#if NO_FEATURE_STATIC_DELEGATE
        private static readonly CacheDict<Type, Func<LightLambda, Delegate>> _runCache = new CacheDict<Type, Func<LightLambda, Delegate>>(100);
#endif

        // Adaptive compilation support
        private readonly LightDelegateCreator _delegateCreator;

        internal LightLambda(LightDelegateCreator delegateCreator, IStrongBox[]? closure)
        {
            _delegateCreator = delegateCreator;
            _closure = closure;
            _interpreter = delegateCreator.Interpreter;
        }

        private string DebugView => new DebugViewPrinter(_interpreter).ToString();

        private sealed class DebugViewPrinter
        {
            private readonly Interpreter _interpreter;
            private readonly Dictionary<int, int> _tryStart = new Dictionary<int, int>();
            private readonly Dictionary<int, string> _handlerEnter = new Dictionary<int, string>();
            private readonly Dictionary<int, int> _handlerExit = new Dictionary<int, int>();
            private string _indent = "  ";

            public DebugViewPrinter(Interpreter interpreter)
            {
                _interpreter = interpreter;

                Analyze();
            }

            private void Analyze()
            {
                Instruction[] instructions = _interpreter.Instructions.Instructions;

                foreach (Instruction instruction in instructions)
                {
                    if (instruction is EnterTryCatchFinallyInstruction enterTryCatchFinally)
                    {
                        TryCatchFinallyHandler handler = enterTryCatchFinally.Handler!;

                        AddTryStart(handler.TryStartIndex);
                        AddHandlerExit(handler.TryEndIndex + 1 /* include Goto instruction that acts as a "leave" */);

                        if (handler.IsFinallyBlockExist)
                        {
                            _handlerEnter.Add(handler.FinallyStartIndex, "finally");
                            AddHandlerExit(handler.FinallyEndIndex);
                        }

                        if (handler.IsCatchBlockExist)
                        {
                            foreach (ExceptionHandler catchHandler in handler.Handlers!)
                            {
                                _handlerEnter.Add(catchHandler.HandlerStartIndex - 1 /* include EnterExceptionHandler instruction */, catchHandler.ToString());
                                AddHandlerExit(catchHandler.HandlerEndIndex);

                                ExceptionFilter? filter = catchHandler.Filter;
                                if (filter != null)
                                {
                                    _handlerEnter.Add(filter.StartIndex - 1 /* include EnterExceptionFilter instruction */, "filter");
                                    AddHandlerExit(filter.EndIndex);
                                }
                            }
                        }
                    }

                    if (instruction is EnterTryFaultInstruction enterTryFault)
                    {
                        TryFaultHandler handler = enterTryFault.Handler!;

                        AddTryStart(handler.TryStartIndex);
                        AddHandlerExit(handler.TryEndIndex + 1 /* include Goto instruction that acts as a "leave" */);

                        _handlerEnter.Add(handler.FinallyStartIndex, "fault");
                        AddHandlerExit(handler.FinallyEndIndex);
                    }
                }
            }

            private void AddTryStart(int index)
            {
                int count;
                if (!_tryStart.TryGetValue(index, out count))
                {
                    _tryStart.Add(index, 1);
                    return;
                }

                _tryStart[index] = count + 1;
            }

            private void AddHandlerExit(int index)
            {
                int count;
                _handlerExit[index] = _handlerExit.TryGetValue(index, out count) ? count + 1 : 1;
            }

            private void Indent()
            {
                _indent = new string(' ', _indent.Length + 2);
            }

            private void Dedent()
            {
                _indent = new string(' ', _indent.Length - 2);
            }

            public override string ToString()
            {
                var sb = new StringBuilder();

                string name = _interpreter.Name ?? "lambda_method";
                sb.Append("object ").Append(name).AppendLine("(object[])");
                sb.AppendLine("{");

                sb.Append("  .locals ").Append(_interpreter.LocalCount).AppendLine();
                sb.Append("  .maxstack ").Append(_interpreter.Instructions.MaxStackDepth).AppendLine();
                sb.Append("  .maxcontinuation ").Append(_interpreter.Instructions.MaxContinuationDepth).AppendLine();
                sb.AppendLine();

                Instruction[] instructions = _interpreter.Instructions.Instructions;
                InstructionArray.DebugView debugView = new InstructionArray.DebugView(_interpreter.Instructions);
                InstructionList.DebugView.InstructionView[] instructionViews = debugView.GetInstructionViews(includeDebugCookies: false);

                for (int i = 0; i < instructions.Length; i++)
                {
                    EmitExits(sb, i);

                    int startCount;
                    if (_tryStart.TryGetValue(i, out startCount))
                    {
                        for (int j = 0; j < startCount; j++)
                        {
                            sb.Append(_indent).AppendLine(".try");
                            sb.Append(_indent).AppendLine("{");
                            Indent();
                        }
                    }

                    string? handler;
                    if (_handlerEnter.TryGetValue(i, out handler))
                    {
                        sb.Append(_indent).AppendLine(handler);
                        sb.Append(_indent).AppendLine("{");
                        Indent();
                    }

                    InstructionList.DebugView.InstructionView instructionView = instructionViews[i];

                    sb.AppendLine(CultureInfo.InvariantCulture, $"{_indent}IP_{i.ToString().PadLeft(4, '0')}: {instructionView.GetValue()}");
                }

                EmitExits(sb, instructions.Length);

                sb.AppendLine("}");

                return sb.ToString();
            }

            private void EmitExits(StringBuilder sb, int index)
            {
                int exitCount;
                if (_handlerExit.TryGetValue(index, out exitCount))
                {
                    for (int j = 0; j < exitCount; j++)
                    {
                        Dedent();
                        sb.Append(_indent).AppendLine("}");
                    }
                }
            }
        }

#if NO_FEATURE_STATIC_DELEGATE
        private static Func<LightLambda, Delegate> GetRunDelegateCtor(Type delegateType)
        {
            lock (_runCache)
            {
                Func<LightLambda, Delegate> fastCtor;
                if (_runCache.TryGetValue(delegateType, out fastCtor))
                {
                    return fastCtor;
                }
                return MakeRunDelegateCtor(delegateType);
            }
        }

        private static Func<LightLambda, Delegate> MakeRunDelegateCtor(Type delegateType)
        {
            MethodInfo method = delegateType.GetInvokeMethod();
            ParameterInfo[] paramInfos = method.GetParametersCached();
            Type[] paramTypes;
            string name = "Run";

            if (paramInfos.Length >= MaxParameters)
            {
                return null;
            }

            if (method.ReturnType == typeof(void))
            {
                name += "Void";
                paramTypes = new Type[paramInfos.Length];
            }
            else
            {
                paramTypes = new Type[paramInfos.Length + 1];
                paramTypes[paramTypes.Length - 1] = method.ReturnType;
            }

            MethodInfo runMethod;

            if (method.ReturnType == typeof(void) && paramTypes.Length == 2 &&
                paramInfos[0].ParameterType.IsByRef && paramInfos[1].ParameterType.IsByRef)
            {
                runMethod = typeof(LightLambda).GetMethod("RunVoidRef2", BindingFlags.NonPublic | BindingFlags.Instance);
                paramTypes[0] = paramInfos[0].ParameterType.GetElementType();
                paramTypes[1] = paramInfos[1].ParameterType.GetElementType();
            }
            else if (method.ReturnType == typeof(void) && paramTypes.Length == 0)
            {
                runMethod = typeof(LightLambda).GetMethod("RunVoid0", BindingFlags.NonPublic | BindingFlags.Instance);
            }
            else
            {
                for (int i = 0; i < paramInfos.Length; i++)
                {
                    paramTypes[i] = paramInfos[i].ParameterType;
                    if (paramTypes[i].IsByRef)
                    {
                        return null;
                    }
                }

#if FEATURE_MAKE_RUN_METHODS
                if (DelegateHelpers.MakeDelegate(paramTypes) == delegateType)
                {
                    name = "Make" + name + paramInfos.Length;

                    MethodInfo ctorMethod = typeof(LightLambda).GetMethod(name, BindingFlags.NonPublic | BindingFlags.Static).MakeGenericMethod(paramTypes);
                    return _runCache[delegateType] = (Func<LightLambda, Delegate>)ctorMethod.CreateDelegate(typeof(Func<LightLambda, Delegate>));
                }
#endif

                runMethod = typeof(LightLambda).GetMethod(name + paramInfos.Length, BindingFlags.NonPublic | BindingFlags.Instance);
            }

            /*
            try {
                DynamicMethod dm = new DynamicMethod("FastCtor", typeof(Delegate), new[] { typeof(LightLambda) }, typeof(LightLambda), true);
                var ilgen = dm.GetILGenerator();
                ilgen.Emit(OpCodes.Ldarg_0);
                ilgen.Emit(OpCodes.Ldftn, runMethod.IsGenericMethodDefinition ? runMethod.MakeGenericMethod(paramTypes) : runMethod);
                ilgen.Emit(OpCodes.Newobj, delegateType.GetConstructor(new[] { typeof(object), typeof(IntPtr) }));
                ilgen.Emit(OpCodes.Ret);
                return _runCache[delegateType] = (Func<LightLambda, Delegate>)dm.CreateDelegate(typeof(Func<LightLambda, Delegate>));
            } catch (SecurityException) {
            }*/

            // we don't have permission for restricted skip visibility dynamic methods, use the slower Delegate.CreateDelegate.
            var targetMethod = runMethod.IsGenericMethodDefinition ? runMethod.MakeGenericMethod(paramTypes) : runMethod;
            return _runCache[delegateType] = lambda => targetMethod.CreateDelegate(delegateType, lambda);
        }

        //TODO enable sharing of these custom delegates
        private Delegate CreateCustomDelegate(Type delegateType)
        {
            //PerfTrack.NoteEvent(PerfTrack.Categories.Compiler, "Synchronously compiling a custom delegate");

            MethodInfo method = delegateType.GetInvokeMethod();
            ParameterInfo[] paramInfos = method.GetParametersCached();
            var parameters = new ParameterExpression[paramInfos.Length];
            var parametersAsObject = new Expression[paramInfos.Length];
            bool hasByRef = false;
            for (int i = 0; i < paramInfos.Length; i++)
            {
                ParameterExpression parameter = Expression.Parameter(paramInfos[i].ParameterType, paramInfos[i].Name);
                hasByRef = hasByRef || paramInfos[i].ParameterType.IsByRef;
                parameters[i] = parameter;
                parametersAsObject[i] = Expression.Convert(parameter, typeof(object));
            }

            NewArrayExpression data = Expression.NewArrayInit(typeof(object), parametersAsObject);
            var dlg = new Func<object[], object>(Run);

            ConstantExpression dlgExpr = Expression.Constant(dlg);

            ParameterExpression argsParam = Expression.Parameter(typeof(object[]), "$args");

            Expression body;
            if (method.ReturnType == typeof(void))
            {
                body = Expression.Block(typeof(void), Expression.Invoke(dlgExpr, argsParam));
            }
            else
            {
                body = Expression.Convert(Expression.Invoke(dlgExpr, argsParam), method.ReturnType);
            }

            if (hasByRef)
            {
                List<Expression> updates = new List<Expression>();
                for (int i = 0; i < paramInfos.Length; i++)
                {
                    if (paramInfos[i].ParameterType.IsByRef)
                    {
                        updates.Add(
                            Expression.Assign(
                                parameters[i],
                                Expression.Convert(
                                    Expression.ArrayAccess(argsParam, Expression.Constant(i)),
                                    paramInfos[i].ParameterType.GetElementType()
                                )
                            )
                        );
                    }
                }

                body = Expression.TryFinally(body, Expression.Block(typeof(void), updates));
            }

            body = Expression.Block(
                method.ReturnType,
                new[] { argsParam },
                Expression.Assign(argsParam, data),
                body
            );

            var lambda = Expression.Lambda(delegateType, body, parameters);
            //return System.Linq.Expressions.Compiler.LambdaCompiler.Compile(lambda, null);
            throw new NotImplementedException("byref delegate");
        }
#endif

        internal Delegate MakeDelegate(Type delegateType)
        {
#if !NO_FEATURE_STATIC_DELEGATE
            MethodInfo method = delegateType.GetInvokeMethod();
            if (method.ReturnType == typeof(void))
            {
                return System.Dynamic.Utils.DelegateHelpers.CreateObjectArrayDelegate(delegateType, RunVoid);
            }
            else
            {
                return System.Dynamic.Utils.DelegateHelpers.CreateObjectArrayDelegate(delegateType, Run);
            }
#else
            Func<LightLambda, Delegate> fastCtor = GetRunDelegateCtor(delegateType);
            if (fastCtor != null)
            {
                return fastCtor(this);
            }
            else
            {
                return CreateCustomDelegate(delegateType);
            }
#endif
        }

        private InterpretedFrame MakeFrame()
        {
            return new InterpretedFrame(_interpreter, _closure);
        }

#if NO_FEATURE_STATIC_DELEGATE
        internal void RunVoidRef2<T0, T1>(ref T0 arg0, ref T1 arg1)
        {
            // copy in and copy out for today...
            var frame = MakeFrame();
            frame.Data[0] = arg0;
            frame.Data[1] = arg1;
            var currentFrame = frame.Enter();
            try
            {
                _interpreter.Run(frame);
            }
            finally
            {
                frame.Leave(currentFrame);
                arg0 = (T0)frame.Data[0];
                arg1 = (T1)frame.Data[1];
            }
        }
#endif

        public object? Run(params object?[] arguments)
        {
            InterpretedFrame frame = MakeFrame();
            for (int i = 0; i < arguments.Length; i++)
            {
                frame.Data[i] = arguments[i];
            }
            InterpretedFrame? currentFrame = frame.Enter();
            try
            {
                _interpreter.Run(frame);
            }
            finally
            {
                for (int i = 0; i < arguments.Length; i++)
                {
                    arguments[i] = frame.Data[i];
                }

                InterpretedFrame.Leave(currentFrame);
            }
            return frame.Pop();
        }

        public object? RunVoid(params object?[] arguments)
        {
            InterpretedFrame frame = MakeFrame();
            for (int i = 0; i < arguments.Length; i++)
            {
                frame.Data[i] = arguments[i];
            }
            InterpretedFrame? currentFrame = frame.Enter();
            try
            {
                _interpreter.Run(frame);
            }
            finally
            {
                for (int i = 0; i < arguments.Length; i++)
                {
                    arguments[i] = frame.Data[i];
                }

                InterpretedFrame.Leave(currentFrame);
            }
            return null;
        }
    }
}
