#if NET_4_6 && !NET_STANDARD_2_0
#define QC_SUPPORTED
#endif

#if QC_SUPPORTED && !QC_DISABLED && !QC_DISABLE_BUILTIN_ALL && !QC_DISABLE_BUILTIN_EXTRA
using System;
using System.CodeDom.Compiler;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;

namespace QFSW.QC
{
    public static class DynamicCodeCommands
    {
        private const Platform execAvailability = Platform.AllPlatforms ^ (Platform.WebGLPlayer | Platform.IPhonePlayer | Platform.XboxOne | Platform.PS4 | Platform.Switch);

        [CommandDescription("Loads the code at the specified file and compiles it to C# which will then be executed. Use with caution as no safety checks will be performed. Not supported in AOT (IL2CPP) builds." +
        "\n\nBy default, boiler plate code will NOT be inserted around the code you provide. Please see 'exec' for more information about boilerplate insertion")]
        [Command("exec-extern", execAvailability)]
        private static async Task ExecuteExternalArbitaryCodeAsync(string filePath, bool insertBoilerplate = false)
        {
            if (!File.Exists(filePath)) { throw new ArgumentException($"file at the specified path '{filePath}' did not exist."); }
            string code = File.ReadAllText(filePath);
            await ExecuteArbitaryCodeAsync(code.Replace("”", "\"").Replace("“", "\""), insertBoilerplate);
        }

        [CommandDescription("Compiles the given code to C# which will then be executed. Use with caution as no safety checks will be performed. Not supported in AOT (IL2CPP) builds." +
        "\n\nBy default, boiler plate code will be inserted around the code you provide. This means various namespaces will be included, and the main class and main function entry point will " +
        "provided. In this case, the code you provide should be code that would exist within the body of the main function, and thus cannot contain things such as class definition. If you " +
        "disable boiler plate insertion, you can write whatever code you want, however you must provide a static entry point called Main in a static class called Program")]
        [Command("exec", execAvailability)]
        private static async Task ExecuteArbitaryCodeAsync(string code, bool insertBoilerplate = true)
        {
#if !UNITY_EDITOR && ENABLE_IL2CPP
            await Task.FromException(new Exception("exec is not supported on AOT platforms such as IL2CPP and requires JIT (Mono)."));
#else
            MethodInfo entryPoint = await Task.Run(() =>
            {
                string fullCode = string.Empty;
                if (insertBoilerplate)
                {
                    string[] includedNamespaces = new string[] { "System", "System.Collections", "System.Collections.Generic",
                                                         "System.Reflection", "System.Linq", "System.Text", "System.Globalization",
                                                         "UnityEngine", "UnityEngine.Events", "UnityEngine.EventSystems", "UnityEngine.UI" };

                    for (int i = 0; i < includedNamespaces.Length; i++) { fullCode += $"using {includedNamespaces[i]};\n"; }
                    fullCode += @"
                        public class Program
                        {
                            public static void Main()
                            {"
                                     + code +
                                @"}
                        }";
                }
                else { fullCode = code; }

                Assembly assembly = CompileCode(fullCode);
                BindingFlags searchFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static;
                Type program = assembly.GetType("Program");
                if (program == null) { throw new ArgumentException("Code Execution Failure - required static class Program could not be found"); }
                entryPoint = program.GetMethod("Main", searchFlags);
                if (entryPoint == null) { throw new ArgumentException("Code Execution Failure - required static entry point Main could not be found"); }
                return entryPoint;
            });

            entryPoint.Invoke(null, null);
#endif
        }

        private static Assembly CompileCode(string code)
        {
#if !UNITY_EDITOR && ENABLE_IL2CPP
            throw new Exception("Code compilation is not supported on AOT platforms such as IL2CPP and requires JIT (Mono).");
#else
            CSharpCompiler.CodeCompiler compiler = new CSharpCompiler.CodeCompiler();
            CompilerParameters compilerParams = new CompilerParameters();
            Assembly[] allLoadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();

            compilerParams.GenerateExecutable = false;
            compilerParams.GenerateInMemory = true;
            for (int i = 0; i < allLoadedAssemblies.Length; i++)
            {
                if (!allLoadedAssemblies[i].IsDynamic)
                {
                    string dllName = allLoadedAssemblies[i].Location;
                    compilerParams.ReferencedAssemblies.Add(dllName);
                }
            }

            CompilerResults compiledCode = compiler.CompileAssemblyFromSource(compilerParams, code);

            if (compiledCode.Errors.HasErrors)
            {
                string errorMessage = "Code Compilation Failure";
                for (int i = 0; i < compiledCode.Errors.Count; i++)
                {
                    errorMessage += $"\n{compiledCode.Errors[i].ErrorNumber} - {compiledCode.Errors[i].ErrorText}";
                }

                throw new ArgumentException(errorMessage);
            }

            return compiledCode.CompiledAssembly;
#endif
        }
    }
}
#endif
