// hook via ILPostProcessor from Unity 2020.3+
// (2020.1 has errors https://github.com/vis2k/Mirror/issues/2912)
#if UNITY_2020_3_OR_NEWER
// Unity.CompilationPipeline reference is only resolved if assembly name is
// Unity.*.CodeGen:
// https://forum.unity.com/threads/how-does-unity-do-codegen-and-why-cant-i-do-it-myself.853867/#post-5646937
using System.IO;
using System.Linq;
// to use Mono.CecilX here, we need to 'override references' in the
// Unity.Mirror.CodeGen assembly definition file in the Editor, and add CecilX.
// otherwise we get a reflection exception with 'file not found: CecilX'.
using Mono.CecilX;
using Mono.CecilX.Cil;
using Unity.CompilationPipeline.Common.ILPostProcessing;
// IMPORTANT: 'using UnityEngine' does not work in here.
// Unity gives "(0,0): error System.Security.SecurityException: ECall methods must be packaged into a system module."
//using UnityEngine;

namespace Mirror.Weaver
{
    public class ILPostProcessorHook : ILPostProcessor
    {
        // from CompilationFinishedHook
        const string MirrorRuntimeAssemblyName = "Mirror";

        // ILPostProcessor is invoked by Unity.
        // we can not tell it to ignore certain assemblies before processing.
        // add a 'ignore' define for convenience.
        // => WeaverTests/WeaverAssembler need it to avoid Unity running it
        public const string IgnoreDefine = "ILPP_IGNORE";

        // we can't use Debug.Log in ILPP, so we need a custom logger
        ILPostProcessorLogger Log = new ILPostProcessorLogger();

        // ???
        public override ILPostProcessor GetInstance() => this;

        // check if assembly has the 'ignore' define
        static bool HasDefine(ICompiledAssembly assembly, string define) =>
            assembly.Defines != null &&
            assembly.Defines.Contains(define);

        // process Mirror, or anything that references Mirror
        public override bool WillProcess(ICompiledAssembly compiledAssembly)
        {
            // compiledAssembly.References are file paths:
            //   Library/Bee/artifacts/200b0aE.dag/Mirror.CompilerSymbols.dll
            //   Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.dll
            //   /Applications/Unity/Hub/Editor/2021.2.0b6_apple_silicon/Unity.app/Contents/NetStandard/ref/2.1.0/netstandard.dll
            //
            // log them to see:
            //     foreach (string reference in compiledAssembly.References)
            //         LogDiagnostics($"{compiledAssembly.Name} references {reference}");
            bool relevant = compiledAssembly.Name == MirrorRuntimeAssemblyName ||
                            compiledAssembly.References.Any(filePath => Path.GetFileNameWithoutExtension(filePath) == MirrorRuntimeAssemblyName);
            bool ignore = HasDefine(compiledAssembly, IgnoreDefine);
            return relevant && !ignore;
        }

        public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly)
        {
            //Log.Warning($"Processing {compiledAssembly.Name}");

            // load the InMemoryAssembly peData into a MemoryStream
            byte[] peData = compiledAssembly.InMemoryAssembly.PeData;
            //LogDiagnostics($"  peData.Length={peData.Length} bytes");
            using (MemoryStream stream = new MemoryStream(peData))
            using (ILPostProcessorAssemblyResolver asmResolver = new ILPostProcessorAssemblyResolver(compiledAssembly, Log))
            {
                // we need to load symbols. otherwise we get:
                // "(0,0): error Mono.CecilX.Cil.SymbolsNotFoundException: No symbol found for file: "
                using (MemoryStream symbols = new MemoryStream(compiledAssembly.InMemoryAssembly.PdbData))
                {
                    ReaderParameters readerParameters = new ReaderParameters{
                        SymbolStream = symbols,
                        ReadWrite = true,
                        ReadSymbols = true,
                        AssemblyResolver = asmResolver,
                        // custom reflection importer to fix System.Private.CoreLib
                        // not being found in custom assembly resolver above.
                        ReflectionImporterProvider = new ILPostProcessorReflectionImporterProvider()
                    };
                    using (AssemblyDefinition asmDef = AssemblyDefinition.ReadAssembly(stream, readerParameters))
                    {
                        // resolving a Mirror.dll type like NetworkServer while
                        // weaving Mirror.dll does not work. it throws a
                        // NullReferenceException in WeaverTypes.ctor
                        // when Resolve() is called on the first Mirror type.
                        // need to add the AssemblyDefinition itself to use.
                        asmResolver.SetAssemblyDefinitionForCompiledAssembly(asmDef);

                        // weave this assembly.
                        Weaver weaver = new Weaver(Log);
                        if (weaver.Weave(asmDef, asmResolver, out bool modified))
                        {
                            //Log.Warning($"Weaving succeeded for: {compiledAssembly.Name}");

                            // write if modified
                            if (modified)
                            {
                                // when weaving Mirror.dll with ILPostProcessor,
                                // Weave() -> WeaverTypes -> resolving the first
                                // type in Mirror.dll adds a reference to
                                // Mirror.dll even though we are in Mirror.dll.
                                // -> this would throw an exception:
                                //    "Mirror references itself" and not compile
                                // -> need to detect and fix manually here
                                if (asmDef.MainModule.AssemblyReferences.Any(r => r.Name == asmDef.Name.Name))
                                {
                                    asmDef.MainModule.AssemblyReferences.Remove(asmDef.MainModule.AssemblyReferences.First(r => r.Name == asmDef.Name.Name));
                                    //Log.Warning($"fixed self referencing Assembly: {asmDef.Name.Name}");
                                }

                                MemoryStream peOut = new MemoryStream();
                                MemoryStream pdbOut = new MemoryStream();
                                WriterParameters writerParameters = new WriterParameters
                                {
                                    SymbolWriterProvider = new PortablePdbWriterProvider(),
                                    SymbolStream = pdbOut,
                                    WriteSymbols = true
                                };

                                asmDef.Write(peOut, writerParameters);

                                InMemoryAssembly inMemory = new InMemoryAssembly(peOut.ToArray(), pdbOut.ToArray());
                                return new ILPostProcessResult(inMemory, Log.Logs);
                            }
                        }
                        // if anything during Weave() fails, we log an error.
                        // don't need to indicate 'weaving failed' again.
                        // in fact, this would break tests only expecting certain errors.
                        //else Log.Error($"Weaving failed for: {compiledAssembly.Name}");
                    }
                }
            }

            // always return an ILPostProcessResult with Logs.
            // otherwise we won't see Logs if weaving failed.
            return new ILPostProcessResult(compiledAssembly.InMemoryAssembly, Log.Logs);
        }
    }
}
#endif
