using FishNet.Broadcast;
using FishNet.CodeGenerating.Extension;
using FishNet.CodeGenerating.Helping;
using FishNet.CodeGenerating.Helping.Extension;
using FishNet.CodeGenerating.Processing;
using FishNet.CodeGenerating.Processing.Rpc;
using FishNet.Configuring;
using FishNet.Serializing.Helping;
using MonoFN.Cecil;
using MonoFN.Cecil.Cil;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Unity.CompilationPipeline.Common.ILPostProcessing;

namespace FishNet.CodeGenerating.ILCore
{
    public class FishNetILPP : ILPostProcessor
    {
        #region Const.
        internal const string RUNTIME_ASSEMBLY_NAME = "FishNet.Runtime";
        #endregion

        public override bool WillProcess(ICompiledAssembly compiledAssembly)
        {
            if (compiledAssembly.Name.StartsWith("Unity."))
                return false;
            if (compiledAssembly.Name.StartsWith("UnityEngine."))
                return false;
            if (compiledAssembly.Name.StartsWith("UnityEditor."))
                return false;
            if (compiledAssembly.Name.Contains("Editor"))
                return false;

            /* This line contradicts the one below where referencesFishNet
             * becomes true if the assembly is FishNetAssembly. This is here
             * intentionally to stop codegen from running on the runtime
             * fishnet assembly, but the option below is for debugging. I would
             * comment out this check if I wanted to compile fishnet runtime. */
            //if (CODEGEN_THIS_NAMESPACE.Length == 0)
            //{
            //    if (compiledAssembly.Name == RUNTIME_ASSEMBLY_NAME)
            //        return false;
            //}
            bool referencesFishNet = FishNetILPP.IsFishNetAssembly(compiledAssembly) || compiledAssembly.References.Any(filePath => Path.GetFileNameWithoutExtension(filePath) == RUNTIME_ASSEMBLY_NAME);
            return referencesFishNet;
        }
        public override ILPostProcessor GetInstance() => this;

        public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly)
        {
            AssemblyDefinition assemblyDef = ILCoreHelper.GetAssemblyDefinition(compiledAssembly);
            if (assemblyDef == null)
                return null;

            //Check WillProcess again; somehow certain editor scripts skip the WillProcess check.
            if (!WillProcess(compiledAssembly))
                return null;

            CodegenSession session = new CodegenSession();
            if (!session.Initialize(assemblyDef.MainModule))
                return null;

            bool modified = false;

            bool fnAssembly = IsFishNetAssembly(compiledAssembly);
            if (fnAssembly)
                modified |= ModifyMakePublicMethods(session);
            /* If one or more scripts use RPCs but don't inherit NetworkBehaviours
             * then don't bother processing the rest. */
            if (session.GetClass<NetworkBehaviourProcessor>().NonNetworkBehaviourHasInvalidAttributes(session.Module.Types))
                return new ILPostProcessResult(null, session.Diagnostics);

            modified |= session.GetClass<WriterProcessor>().Process();
            modified |= session.GetClass<ReaderProcessor>().Process();
            modified |= CreateDeclaredSerializerDelegates(session);
            modified |= CreateDeclaredSerializers(session);
            modified |= CreateDeclaredComparerDelegates(session);
            modified |= CreateIBroadcast(session);
            modified |= CreateQOLAttributes(session);
            modified |= CreateNetworkBehaviours(session);
            modified |= CreateGenericReadWriteDelegates(session);

            if (fnAssembly)
            {
                AssemblyNameReference anr = session.Module.AssemblyReferences.FirstOrDefault<AssemblyNameReference>(x => x.FullName == session.Module.Assembly.FullName);
                if (anr != null)
                    session.Module.AssemblyReferences.Remove(anr);
            }

            /* If there are warnings about SyncVars being in different assemblies.
             * This is awful ... codegen would need to be reworked to save
             * syncvars across all assemblies so that scripts referencing them from
             * another assembly can have it's instructions changed. This however is an immense
             * amount of work so it will have to be put on hold, for... a long.. long while. */
            if (session.DifferentAssemblySyncVars.Count > 0)
            {
                StringBuilder sb = new StringBuilder();
                sb.AppendLine($"Assembly {session.Module.Name} has inherited access to SyncVars in different assemblies. When accessing SyncVars across assemblies be sure to use Get/Set methods withinin the inherited assembly script to change SyncVars. Accessible fields are:");

                foreach (FieldDefinition item in session.DifferentAssemblySyncVars)
                    sb.AppendLine($"Field {item.Name} within {item.DeclaringType.FullName} in assembly {item.Module.Name}.");

                session.LogWarning("v------- IMPORTANT -------v");
                session.LogWarning(sb.ToString());
                session.DifferentAssemblySyncVars.Clear();
            }

            //session.LogWarning($"Assembly {compiledAssembly.Name} took {stopwatch.ElapsedMilliseconds}.");
            if (!modified)
            {
                return null;
            }
            else
            {
                MemoryStream pe = new MemoryStream();
                MemoryStream pdb = new MemoryStream();
                WriterParameters writerParameters = new WriterParameters
                {
                    SymbolWriterProvider = new PortablePdbWriterProvider(),
                    SymbolStream = pdb,
                    WriteSymbols = true
                };
                assemblyDef.Write(pe, writerParameters);
                return new ILPostProcessResult(new InMemoryAssembly(pe.ToArray(), pdb.ToArray()), session.Diagnostics);
            }
        }

        /// <summary>
        /// Makees methods public scope which use CodegenMakePublic attribute.
        /// </summary>
        /// <returns></returns>
        private bool ModifyMakePublicMethods(CodegenSession session)
        {
            string makePublicTypeFullName = typeof(CodegenMakePublicAttribute).FullName;
            foreach (TypeDefinition td in session.Module.Types)
            {
                foreach (MethodDefinition md in td.Methods)
                {
                    foreach (CustomAttribute ca in md.CustomAttributes)
                    {
                        if (ca.AttributeType.FullName == makePublicTypeFullName)
                        {
                            md.Attributes &= ~MethodAttributes.Assembly;
                            md.Attributes |= MethodAttributes.Public;
                        }
                    }
                }
            }

            //There is always at least one modified.
            return true;
        }
        /// <summary>
        /// Creates delegates for user declared serializers.
        /// </summary>
        internal bool CreateDeclaredSerializerDelegates(CodegenSession session)
        {
            bool modified = false;

            TypeAttributes readWriteExtensionTypeAttr = (TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Abstract);
            List<TypeDefinition> allTypeDefs = session.Module.Types.ToList();
            foreach (TypeDefinition td in allTypeDefs)
            {
                if (session.GetClass<GeneralHelper>().IgnoreTypeDefinition(td))
                    continue;

                if (td.Attributes.HasFlag(readWriteExtensionTypeAttr))
                    modified |= session.GetClass<CustomSerializerProcessor>().CreateSerializerDelegates(td, true);
            }

            return modified;
        }

        /// <summary>
        /// Creates serializers for custom types within user declared serializers.
        /// </summary>
        private bool CreateDeclaredSerializers(CodegenSession session)
        {
            bool modified = false;

            TypeAttributes readWriteExtensionTypeAttr = (TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Abstract);
            List<TypeDefinition> allTypeDefs = session.Module.Types.ToList();
            foreach (TypeDefinition td in allTypeDefs)
            {
                if (session.GetClass<GeneralHelper>().IgnoreTypeDefinition(td))
                    continue;

                if (td.Attributes.HasFlag(readWriteExtensionTypeAttr))
                    modified |= session.GetClass<CustomSerializerProcessor>().CreateSerializers(td);
            }

            return modified;
        }

        /// <summary>
        /// Creates delegates for user declared comparers.
        /// </summary>
        internal bool CreateDeclaredComparerDelegates(CodegenSession session)
        {
            bool modified = false;
            List<TypeDefinition> allTypeDefs = session.Module.Types.ToList();
            foreach (TypeDefinition td in allTypeDefs)
            {
                if (session.GetClass<GeneralHelper>().IgnoreTypeDefinition(td))
                    continue;

                modified |= session.GetClass<CustomSerializerProcessor>().CreateComparerDelegates(td);
            }

            return modified;
        }


        /// <summary>
        /// Creaters serializers and calls for IBroadcast.
        /// </summary>
        /// <param name="moduleDef"></param>
        /// <param name="diagnostics"></param>
        private bool CreateIBroadcast(CodegenSession session)
        {
            bool modified = false;

            string networkBehaviourFullName = session.GetClass<NetworkBehaviourHelper>().FullName;
            HashSet<TypeDefinition> typeDefs = new HashSet<TypeDefinition>();
            foreach (TypeDefinition td in session.Module.Types)
            {
                TypeDefinition climbTd = td;
                do
                {
                    //Reached NetworkBehaviour class.
                    if (climbTd.FullName == networkBehaviourFullName)
                        break;

                    ///* Check initial class as well all types within
                    // * the class. Then check all of it's base classes. */
                    if (climbTd.ImplementsInterface<IBroadcast>())
                        typeDefs.Add(climbTd);
                    //7ms

                    //Add nested. Only going to go a single layer deep.
                    foreach (TypeDefinition nestedTypeDef in td.NestedTypes)
                    {
                        if (nestedTypeDef.ImplementsInterface<IBroadcast>())
                            typeDefs.Add(nestedTypeDef);
                    }
                    //0ms

                    climbTd = climbTd.GetNextBaseTypeDefinition(session);
                    //this + name check 40ms
                } while (climbTd != null);

            }


            //Create reader/writers for found typeDefs.
            foreach (TypeDefinition td in typeDefs)
            {
                TypeReference typeRef = session.ImportReference(td);
                bool canSerialize = session.GetClass<GeneralHelper>().HasSerializerAndDeserializer(typeRef, true);
                if (!canSerialize)
                    session.LogError($"Broadcast {td.Name} does not support serialization. Use a supported type or create a custom serializer.");
                else
                    modified = true;
            }

            return modified;
        }

        /// <summary>
        /// Handles QOLAttributes such as [Server].
        /// </summary>
        /// <returns></returns>
        private bool CreateQOLAttributes(CodegenSession session)
        {
            bool modified = false;

            bool codeStripping = false;
            
            List<TypeDefinition> allTypeDefs = session.Module.Types.ToList();

            /* First pass, potentially only pass.
             * If code stripping them this will be run again. The first iteration
             * is to ensure things are removed in the proper order. */
            foreach (TypeDefinition td in allTypeDefs)
            {
                if (session.GetClass<GeneralHelper>().IgnoreTypeDefinition(td))
                    continue;

                modified |= session.GetClass<QolAttributeProcessor>().Process(td, codeStripping);
            }

            

            return modified;
        }

        /// <summary>
        /// Creates NetworkBehaviour changes.
        /// </summary>
        /// <param name="moduleDef"></param>
        /// <param name="diagnostics"></param>
        private bool CreateNetworkBehaviours(CodegenSession session)
        {
            bool modified = false;
            //Get all network behaviours to process.
            List<TypeDefinition> networkBehaviourTypeDefs = session.Module.Types
                .Where(td => td.IsSubclassOf(session, session.GetClass<NetworkBehaviourHelper>().FullName))
                .ToList();

            //Moment a NetworkBehaviour exist the assembly is considered modified.
            if (networkBehaviourTypeDefs.Count > 0)
                modified = true;

            /* Remove types which are inherited. This gets the child most networkbehaviours.
             * Since processing iterates all parent classes there's no reason to include them */
            RemoveInheritedTypeDefinitions(networkBehaviourTypeDefs);
            //Set how many rpcs are in children classes for each typedef.
            Dictionary<TypeDefinition, uint> inheritedRpcCounts = new Dictionary<TypeDefinition, uint>();
            SetChildRpcCounts(inheritedRpcCounts, networkBehaviourTypeDefs);
            //Set how many synctypes are in children classes for each typedef.
            Dictionary<TypeDefinition, uint> inheritedSyncTypeCounts = new Dictionary<TypeDefinition, uint>();
            SetChildSyncTypeCounts(inheritedSyncTypeCounts, networkBehaviourTypeDefs);

            /* This holds all sync types created, synclist, dictionary, var
             * and so on. This data is used after all syncvars are made so
             * other methods can look for references to created synctypes and
             * replace accessors accordingly. */
            List<(SyncType, ProcessedSync)> allProcessedSyncs = new List<(SyncType, ProcessedSync)>();
            HashSet<string> allProcessedCallbacks = new HashSet<string>();
            List<TypeDefinition> processedClasses = new List<TypeDefinition>();

            foreach (TypeDefinition typeDef in networkBehaviourTypeDefs)
            {
                session.ImportReference(typeDef);
                //Synctypes processed for this nb and it's inherited classes.
                List<(SyncType, ProcessedSync)> processedSyncs = new List<(SyncType, ProcessedSync)>();
                session.GetClass<NetworkBehaviourProcessor>().Process(typeDef, processedSyncs,
                    inheritedSyncTypeCounts, inheritedRpcCounts);
                //Add to all processed.
                allProcessedSyncs.AddRange(processedSyncs);
            }

            /* Must run through all scripts should user change syncvar
             * from outside the networkbehaviour. */
            if (allProcessedSyncs.Count > 0)
            {
                foreach (TypeDefinition td in session.Module.Types)
                {
                    session.GetClass<NetworkBehaviourSyncProcessor>().ReplaceGetSets(td, allProcessedSyncs);
                    session.GetClass<RpcProcessor>().RedirectBaseCalls();
                }
            }

            /* Removes typedefinitions which are inherited by
             * another within tds. For example, if the collection
             * td contains A, B, C and our structure is
             * A : B : C then B and C will be removed from the collection
             *  Since they are both inherited by A. */
            void RemoveInheritedTypeDefinitions(List<TypeDefinition> tds)
            {
                HashSet<TypeDefinition> inheritedTds = new HashSet<TypeDefinition>();
                /* Remove any networkbehaviour typedefs which are inherited by
                 * another networkbehaviour typedef. When a networkbehaviour typedef
                 * is processed so are all of the inherited types. */
                for (int i = 0; i < tds.Count; i++)
                {
                    /* Iterates all base types and
                     * adds them to inheritedTds so long
                     * as the base type is not a NetworkBehaviour. */
                    TypeDefinition copyTd = tds[i].GetNextBaseTypeDefinition(session);
                    while (copyTd != null)
                    {
                        //Class is NB.
                        if (copyTd.FullName == session.GetClass<NetworkBehaviourHelper>().FullName)
                            break;

                        inheritedTds.Add(copyTd);
                        copyTd = copyTd.GetNextBaseTypeDefinition(session);
                    }
                }

                //Remove all inherited types.
                foreach (TypeDefinition item in inheritedTds)
                    tds.Remove(item);
            }

            /* Sets how many Rpcs are within the children
             * of each typedefinition. EG: if our structure is
             * A : B : C, with the following RPC counts...
             * A 3
             * B 1
             * C 2
             * then B child rpc counts will be 3, and C will be 4. */
            void SetChildRpcCounts(Dictionary<TypeDefinition, uint> typeDefCounts, List<TypeDefinition> tds)
            {
                foreach (TypeDefinition typeDef in tds)
                {
                    //Number of RPCs found while climbing typeDef.
                    uint childCount = 0;

                    TypeDefinition copyTd = typeDef;
                    do
                    {
                        //How many RPCs are in copyTd.
                        uint copyCount = session.GetClass<RpcProcessor>().GetRpcCount(copyTd);

                        /* If not found it this is the first time being
                         * processed. When this occurs set the value
                         * to 0. It will be overwritten below if baseCount
                         * is higher. */
                        uint previousCopyChildCount = 0;
                        if (!typeDefCounts.TryGetValue(copyTd, out previousCopyChildCount))
                            typeDefCounts[copyTd] = 0;
                        /* If baseCount is higher then replace count for copyTd.
                         * This can occur when a class is inherited by several types
                         * and the first processed type might only have 1 rpc, while
                         * the next has 2. This could be better optimized but to keep
                         * the code easier to read, it will stay like this. */
                        if (childCount > previousCopyChildCount)
                            typeDefCounts[copyTd] = childCount;

                        //Increase baseCount with RPCs found here.
                        childCount += copyCount;

                        copyTd = copyTd.GetNextBaseClassToProcess(session);
                    } while (copyTd != null);
                }

            }


            /* This performs the same functionality as SetChildRpcCounts
             * but for SyncTypes. */
            void SetChildSyncTypeCounts(Dictionary<TypeDefinition, uint> typeDefCounts, List<TypeDefinition> tds)
            {
                foreach (TypeDefinition typeDef in tds)
                {
                    //Number of RPCs found while climbing typeDef.
                    uint childCount = 0;

                    TypeDefinition copyTd = typeDef;
                    /* Iterate up to the parent script and then reverse
                     * the order. This is so that the topmost is 0
                     * and each inerhiting script adds onto that.
                     * Setting child types this way makes it so parent
                     * types don't need to have their synctype/rpc counts
                     * rebuilt when scripts are later to be found
                     * inheriting from them. */
                    List<TypeDefinition> reversedTypeDefs = new List<TypeDefinition>();
                    do
                    {
                        reversedTypeDefs.Add(copyTd);
                        copyTd = copyTd.GetNextBaseClassToProcess(session);
                    } while (copyTd != null);
                    reversedTypeDefs.Reverse();

                    foreach (TypeDefinition td in reversedTypeDefs)
                    {
                        //How many RPCs are in copyTd.
                        uint copyCount = session.GetClass<NetworkBehaviourSyncProcessor>().GetSyncTypeCount(td);
                        /* If not found it this is the first time being
                         * processed. When this occurs set the value
                         * to 0. It will be overwritten below if baseCount
                         * is higher. */
                        uint previousCopyChildCount = 0;
                        if (!typeDefCounts.TryGetValue(td, out previousCopyChildCount))
                            typeDefCounts[td] = 0;
                        /* If baseCount is higher then replace count for copyTd.
                         * This can occur when a class is inherited by several types
                         * and the first processed type might only have 1 rpc, while
                         * the next has 2. This could be better optimized but to keep
                         * the code easier to read, it will stay like this. */
                        if (childCount > previousCopyChildCount)
                            typeDefCounts[td] = childCount;
                        //Increase baseCount with RPCs found here.
                        childCount += copyCount;
                    }
                }
            }


            return modified;
        }

        /// <summary>
        /// Creates generic delegates for all read and write methods.
        /// </summary>
        /// <param name="moduleDef"></param>
        /// <param name="diagnostics"></param>
        private bool CreateGenericReadWriteDelegates(CodegenSession session)
        {
            session.GetClass<WriterProcessor>().CreateStaticMethodDelegates();
            session.GetClass<ReaderProcessor>().CreateStaticMethodDelegates();

            return true;
        }

        internal static bool IsFishNetAssembly(ICompiledAssembly assembly) => (assembly.Name == FishNetILPP.RUNTIME_ASSEMBLY_NAME);
        internal static bool IsFishNetAssembly(CodegenSession session) => (session.Module.Assembly.Name.Name == FishNetILPP.RUNTIME_ASSEMBLY_NAME);
        internal static bool IsFishNetAssembly(ModuleDefinition moduleDef) => (moduleDef.Assembly.Name.Name == FishNetILPP.RUNTIME_ASSEMBLY_NAME);

    }
}