using FishNet.CodeGenerating.Extension;
using FishNet.CodeGenerating.Helping.Extension;
using FishNet.CodeGenerating.ILCore;
using FishNet.Object;
using FishNet.Serializing;
using FishNet.Utility;
using FishNet.Utility.Performance;
using MonoFN.Cecil;
using MonoFN.Cecil.Cil;
using MonoFN.Cecil.Rocks;
using System;
using System.Collections.Generic;
using GameKit.Dependencies.Utilities;
using SR = System.Reflection;
using UnityDebug = UnityEngine.Debug;

namespace FishNet.CodeGenerating.Helping
{
    internal class WriterProcessor : CodegenBase
    {
        #region Reflection references.
        public readonly Dictionary<string, MethodReference> InstancedWriterMethods = new();
        public readonly Dictionary<string, MethodReference> StaticWriterMethods = new();
        public TypeDefinition GeneratedWriterClassTypeDef;
        public MethodDefinition GeneratedWriterOnLoadMethodDef;
        #endregion

        #region Misc.
        /// <summary>
        /// TypeReferences which have already had delegates made for.
        /// </summary>
        private HashSet<TypeReference> _delegatedTypes = new();
        #endregion

        #region Const.
        /// <summary>
        /// Namespace to use for generated serializers and delegates.
        /// </summary>
        public const string GENERATED_WRITER_NAMESPACE = "FishNet.Serializing.Generated";
        /// <summary>
        /// Name to use for generated serializers class.
        /// </summary>
        public const string GENERATED_WRITERS_CLASS_NAME = "GeneratedWriters___Internal";
        /// <summary>
        /// Attributes to use for generated serializers class.
        /// </summary>
        public const TypeAttributes GENERATED_TYPE_ATTRIBUTES = TypeAttributes.BeforeFieldInit | TypeAttributes.Class | TypeAttributes.AnsiClass | TypeAttributes.Public | TypeAttributes.AutoClass | TypeAttributes.Abstract | TypeAttributes.Sealed;
        /// <summary>
        /// Name to use for InitializeOnce method.
        /// </summary>
        public const string INITIALIZEONCE_METHOD_NAME = "InitializeOnce";
        /// <summary>
        /// Attributes to use for InitializeOnce method within generated serializer classes.
        /// </summary>
        public const MethodAttributes INITIALIZEONCE_METHOD_ATTRIBUTES = MethodAttributes.Static | MethodAttributes.Private | MethodAttributes.HideBySig;
        /// <summary>
        /// Attritbutes to use for generated serializers.
        /// </summary>
        public const MethodAttributes GENERATED_METHOD_ATTRIBUTES = MethodAttributes.Static | MethodAttributes.Public | MethodAttributes.HideBySig;
        /// <summary>
        /// Attributes required for custom serializer classes.
        /// </summary>
        public const TypeAttributes CUSTOM_SERIALIZER_TYPEDEF_ATTRIBUTES = TypeAttributes.Sealed | TypeAttributes.Abstract;
        /// <summary>
        /// Prefix all built-in and user created write methods should begin with.
        /// </summary>
        internal const string CUSTOM_WRITER_PREFIX = "Write";
        /// <summary>
        /// Attribute fullname which indicates a default writer.
        /// </summary>
        public string DEFAULT_WRITER_ATTRIBUTE_FULLNAME => typeof(DefaultWriterAttribute).FullName;
        /// <summary>
        /// Types to exclude from being scanned for auto serialization.
        /// </summary>
        public static readonly Type[] EXCLUDED_AUTO_SERIALIZER_TYPES = new Type[]
        {
            typeof(NetworkBehaviour)
        };
        /// <summary>
        /// Types within assemblies which begin with these prefixes will not have serializers created for them.
        /// </summary>
        public static readonly string[] EXCLUDED_ASSEMBLY_PREFIXES = new string[]
        {
            "UnityEngine.",
            "Unity.Mathmatics"
        };
        #endregion

        public override bool ImportReferences() => true;

        /// <summary>
        /// Processes data. To be used after everything else has called ImportReferences.
        /// </summary>
        /// <returns></returns>
        public bool Process()
        {
            GeneralHelper gh = GetClass<GeneralHelper>();

            CreateGeneratedWritersClass();
            FindInstancedWriters();
            CreateInstancedWriterExtensions();

            // Creates class for generated writers, and init on load method.
            void CreateGeneratedWritersClass()
            {
                GeneratedWriterClassTypeDef = gh.GetOrCreateClass(out _, GENERATED_TYPE_ATTRIBUTES, GENERATED_WRITERS_CLASS_NAME, null, GENERATED_WRITER_NAMESPACE);
                /* If constructor isn't set then try to get or create it
                 * and also add it to methods if were created. */
                GeneratedWriterOnLoadMethodDef = gh.GetOrCreateMethod(GeneratedWriterClassTypeDef, out _, INITIALIZEONCE_METHOD_ATTRIBUTES, INITIALIZEONCE_METHOD_NAME, Module.TypeSystem.Void);
                ILProcessor pp = GeneratedWriterOnLoadMethodDef.Body.GetILProcessor();
                pp.Emit(OpCodes.Ret);
                gh.CreateRuntimeInitializeOnLoadMethodAttribute(GeneratedWriterOnLoadMethodDef);
            }

            // Finds all instanced writers and autopack types.
            void FindInstancedWriters()
            {
                Type pooledWriterType = typeof(PooledWriter);
                foreach (SR.MethodInfo methodInfo in pooledWriterType.GetMethods())
                {
                    if (!HasDefaultSerializerAttribute())
                        continue;

                    MethodReference methodRef = ImportReference(methodInfo);
                    /* TypeReference for the first parameter in the write method.
                     * The first parameter will always be the type written. */
                    TypeReference typeRef = ImportReference(methodRef.Parameters[0].ParameterType);
                    /* If here all checks pass. */
                    AddWriterMethod(typeRef, methodRef, true, true);

                    bool HasDefaultSerializerAttribute()
                    {
                        foreach (SR.CustomAttributeData item in methodInfo.CustomAttributes)
                        {
                            if (item.AttributeType.FullName == DEFAULT_WRITER_ATTRIBUTE_FULLNAME)
                                return true;
                        }

                        return false;
                    }
                }
            }

            return true;
        }

        /// <summary>
        /// Creates writer extension methods for built-in writers.
        /// </summary>
        private void CreateInstancedWriterExtensions()
        {
            if (!FishNetILPP.IsFishNetAssembly(Session))
                return;

            GeneralHelper gh = GetClass<GeneralHelper>();
            WriterProcessor gwh = GetClass<WriterProcessor>();

            // List<MethodReference> staticReaders = new List<MethodReference>();
            foreach (KeyValuePair<string, MethodReference> item in InstancedWriterMethods)
            {
                MethodReference instancedWriteMr = item.Value;
                if (instancedWriteMr.HasGenericParameters)
                    continue;

                TypeReference valueTr = instancedWriteMr.Parameters[0].ParameterType;

                MethodDefinition md = new($"InstancedExtension___{instancedWriteMr.Name}", GENERATED_METHOD_ATTRIBUTES, Module.TypeSystem.Void);

                // Add extension parameter.
                ParameterDefinition writerPd = gh.CreateParameter(md, typeof(Writer), "writer");
                // Add parameters needed by instanced writer.
                List<ParameterDefinition> otherPds = md.CreateParameters(Session, instancedWriteMr.CachedResolve(Session));
                gh.MakeExtensionMethod(md);
                //
                gwh.GeneratedWriterClassTypeDef.Methods.Add(md);

                ILProcessor processor = md.Body.GetILProcessor();
                // Load writer.
                processor.Emit(OpCodes.Ldarg, writerPd);
                // Load args.
                foreach (ParameterDefinition pd in otherPds)
                    processor.Emit(OpCodes.Ldarg, pd);
                // Call instanced.
                processor.Emit(instancedWriteMr.GetCallOpCode(Session), instancedWriteMr);
                processor.Emit(OpCodes.Ret);
                AddWriterMethod(valueTr, md, false, true);
            }
        }

        /// <summary>
        /// Adds typeRef, methodDef to Instanced or Static write methods.
        /// </summary>
        public void AddWriterMethod(TypeReference typeRef, MethodReference methodRef, bool instanced, bool useAdd)
        {
            Dictionary<string, MethodReference> dict = instanced ? InstancedWriterMethods : StaticWriterMethods;
            string fullName = typeRef.GetFullnameWithoutBrackets();
            if (useAdd)
            {
                if (dict.ContainsKey(fullName))
                    LogError($"Key {fullName} already exists. First method is {dict[fullName].Name}, new method is {methodRef.Name}.");
                else
                    dict.Add(fullName, methodRef);
            }
            else
            {
                dict[fullName] = methodRef;
            }
        }

        /// <summary>
        /// Removes typeRef from Instanced or Static write methods.
        /// </summary>
        internal void RemoveWriterMethod(TypeReference typeRef, bool instanced)
        {
            Dictionary<string, MethodReference> dict = instanced ? InstancedWriterMethods : StaticWriterMethods;
            string fullName = typeRef.GetFullnameWithoutBrackets();
            dict.Remove(fullName);
        }

        /// <summary>
        /// Creates Write<T> delegates for known static methods.
        /// </summary>
        public void CreateInitializeDelegates()
        {
            foreach (KeyValuePair<string, MethodReference> item in StaticWriterMethods)
                GetClass<WriterProcessor>().CreateInitializeDelegate(item.Value);
        }

        /// <summary>
        /// Creates a Write delegate for writeMethodRef and places it within the generated reader/writer constructor.
        /// </summary>
        /// <param name = "writeMr"></param>
        private void CreateInitializeDelegate(MethodReference writeMr)
        {
            GeneralHelper gh = GetClass<GeneralHelper>();
            WriterImports wi = GetClass<WriterImports>();

            /* If a global serializer is declared for the type
             * and the method is not the declared serializer then
             * exit early. */
            if (DeclaresUseGlobalCustomSerializer(writeMr.Parameters[1].ParameterType) && writeMr.Name.StartsWith(UtilityConstants.GeneratedWriterPrefix))
                return;

            // Check if ret already exist, if so remove it; ret will be added on again in this method.
            if (GeneratedWriterOnLoadMethodDef.Body.Instructions.Count != 0)
            {
                int lastIndex = GeneratedWriterOnLoadMethodDef.Body.Instructions.Count - 1;
                if (GeneratedWriterOnLoadMethodDef.Body.Instructions[lastIndex].OpCode == OpCodes.Ret)
                    GeneratedWriterOnLoadMethodDef.Body.Instructions.RemoveAt(lastIndex);
            }

            ILProcessor processor = GeneratedWriterOnLoadMethodDef.Body.GetILProcessor();
            TypeReference dataTypeRef;
            dataTypeRef = writeMr.Parameters[1].ParameterType;

            // Check if writer already exist.
            if (_delegatedTypes.Contains(dataTypeRef))
            {
                LogError($"Generic write already created for {dataTypeRef.FullName}.");
                return;
            }
            else
            {
                _delegatedTypes.Add(dataTypeRef);
            }

            /* Create a Action<Writer, T> delegate.
             * May also be Action<Writer, AutoPackType, T> delegate
             * for packed types. */
            processor.Emit(OpCodes.Ldnull);
            processor.Emit(OpCodes.Ldftn, writeMr);

            GenericInstanceType actionGenericInstance;
            MethodReference actionConstructorInstanceMethodRef;

            actionGenericInstance = gh.ActionT2_TypeRef.MakeGenericInstanceType(wi.Writer_TypeRef, dataTypeRef);
            actionConstructorInstanceMethodRef = gh.ActionT2Constructor_MethodRef.MakeHostInstanceGeneric(Session, actionGenericInstance);

            processor.Emit(OpCodes.Newobj, actionConstructorInstanceMethodRef);
            // Call delegate to GenericWriter<T>.Write
            GenericInstanceType genericInstance = wi.GenericWriter_TypeRef.MakeGenericInstanceType(dataTypeRef);
            MethodReference genericWriteMethodRef = wi.GenericWriter_Write_MethodRef.MakeHostInstanceGeneric(Session, genericInstance);
            processor.Emit(OpCodes.Call, genericWriteMethodRef);

            processor.Emit(OpCodes.Ret);
        }

        /// <summary>
        /// Returns if typeRef has a serializer.
        /// </summary>
        /// <param name = "typeRef"></param>
        /// <returns></returns>
        internal bool HasSerializer(TypeReference typeRef, bool createMissing)
        {
            bool result = GetInstancedWriteMethodReference(typeRef) != null || GetStaticWriteMethodReference(typeRef) != null || DeclaresUseGlobalCustomSerializer(typeRef);

            if (!result && createMissing)
            {
                if (!GetClass<GeneralHelper>().HasNonSerializableAttribute(typeRef.CachedResolve(Session)))
                { 
                    string traceText = TypeReferenceTraceText(typeRef);
                    
                    MethodReference methodRef = CreateWriter(typeRef, traceText);
                    result = methodRef != null;
                }
            }

            return result;
        }

        #region GetWriterMethodReference.
        /// <summary>
        /// Returns the MethodReference for typeRef.
        /// </summary>
        /// <param name = "typeRef"></param>
        /// <returns></returns>
        internal MethodReference GetInstancedWriteMethodReference(TypeReference typeRef)
        {
            string fullName = typeRef.GetFullnameWithoutBrackets();
            InstancedWriterMethods.TryGetValue(fullName, out MethodReference methodRef);
            return methodRef;
        }

        /// <summary>
        /// Returns the MethodReference for typeRef.
        /// </summary>
        /// <param name = "typeRef"></param>
        /// <returns></returns>
        internal MethodReference GetStaticWriteMethodReference(TypeReference typeRef)
        {
            string fullName = typeRef.GetFullnameWithoutBrackets();
            StaticWriterMethods.TryGetValue(fullName, out MethodReference methodRef);
            return methodRef;
        }

        /// <summary>
        /// Returns the MethodReference for typeRef favoring instanced or static.
        /// </summary>
        /// <param name = "typeRef"></param>
        /// <param name = "favorInstanced"></param>
        /// <returns></returns>
        internal MethodReference GetWriteMethodReference(TypeReference typeRef)
        {
            bool favorInstanced = false;

            MethodReference result;
            if (favorInstanced)
            {
                result = GetInstancedWriteMethodReference(typeRef);
                if (result == null)
                    result = GetStaticWriteMethodReference(typeRef);
            }
            else
            {
                result = GetStaticWriteMethodReference(typeRef);
                if (result == null)
                    result = GetInstancedWriteMethodReference(typeRef);
            }

            return result;
        }

        /// <summary>
        /// Gets the write MethodRef for typeRef, or tries to create it if not present.
        /// </summary>
        /// <param name = "typeRef"></param>
        /// <returns></returns>
        internal MethodReference GetOrCreateWriteMethodReference(TypeReference typeRef, string traceText)
        {
            // Try to get existing writer, if not present make one.
            MethodReference writeMethodRef = GetWriteMethodReference(typeRef);
            if (writeMethodRef == null)
                writeMethodRef = CreateWriter(typeRef, traceText);

            // If still null then return could not be generated.
            if (writeMethodRef == null)
            {
                LogError($"Could not create serializer for {typeRef.FullName}.");
            }
            // Otherwise, check if generic and create writes for generic pararameters.
            else if (typeRef.IsGenericInstance)
            {
                GenericInstanceType git = (GenericInstanceType)typeRef;
                foreach (TypeReference item in git.GenericArguments)
                {
                    MethodReference result = GetOrCreateWriteMethodReference(item, traceText);
                    if (result == null)
                    {
                        LogError($"Could not create serializer for {item.FullName}.");
                        return null;
                    }
                }
            }

            return writeMethodRef;
        }
        #endregion

        /// <summary>
        /// Creates a PooledWriter within the body/ and returns its variable index.
        /// EG: PooledWriter writer = WriterPool.RetrieveWriter();
        /// </summary>
        internal VariableDefinition CreatePooledWriter(MethodDefinition methodDef, int length)
        {
            VariableDefinition resultVd;
            List<Instruction> insts = CreatePooledWriter(methodDef, length, out resultVd);

            ILProcessor processor = methodDef.Body.GetILProcessor();
            processor.Add(insts);
            return resultVd;
        }

        /// <summary>
        /// Creates a PooledWriter within the body/ and returns its variable index.
        /// EG: PooledWriter writer = WriterPool.RetrieveWriter();
        /// </summary>
        /// <param name = "processor"></param>
        /// <param name = "methodDef"></param>
        /// <returns></returns>
        internal List<Instruction> CreatePooledWriter(MethodDefinition methodDef, int length, out VariableDefinition resultVd)
        {
            WriterImports wi = GetClass<WriterImports>();

            List<Instruction> insts = new();
            ILProcessor processor = methodDef.Body.GetILProcessor();

            resultVd = GetClass<GeneralHelper>().CreateVariable(methodDef, wi.PooledWriter_TypeRef);
            // If length is specified then pass in length.
            if (length > 0)
            {
                insts.Add(processor.Create(OpCodes.Ldc_I4, length));
                insts.Add(processor.Create(OpCodes.Call, wi.WriterPool_GetWriterLength_MethodRef));
            }
            // Use parameter-less method if no length.
            else
            {
                insts.Add(processor.Create(OpCodes.Call, wi.WriterPool_GetWriter_MethodRef));
            }

            // Set value to variable definition.
            insts.Add(processor.Create(OpCodes.Stloc, resultVd));
            return insts;
        }

        /// <summary>
        /// Calls Dispose on a PooledWriter.
        /// EG: writer.Dispose();
        /// </summary>
        /// <param name = "processor"></param>
        /// <param name = "writerDefinition"></param>
        internal List<Instruction> DisposePooledWriter(MethodDefinition methodDef, VariableDefinition writerDefinition)
        {
            WriterImports wi = GetClass<WriterImports>();

            List<Instruction> insts = new();
            ILProcessor processor = methodDef.Body.GetILProcessor();

            insts.Add(processor.Create(OpCodes.Ldloc, writerDefinition));
            insts.Add(processor.Create(wi.PooledWriter_Dispose_MethodRef.GetCallOpCode(Session), wi.PooledWriter_Dispose_MethodRef));

            return insts;
        }

        /// <summary>
        /// Creates a null check on the second argument using a boolean.
        /// </summary>
        internal void CreateRetOnNull(ILProcessor processor, ParameterDefinition writerParameterDef, ParameterDefinition checkedParameterDef)
        {
            Instruction endIf = processor.Create(OpCodes.Nop);
            // If (value) jmp to endIf.
            processor.Emit(OpCodes.Ldarg, checkedParameterDef);
            processor.Emit(OpCodes.Brtrue, endIf);
            // writer.WriteBool
            CreateWriteBool(processor, writerParameterDef, true);
            // Exit method.
            processor.Emit(OpCodes.Ret);
            // End of if check.
            processor.Append(endIf);
        }

        /// <summary>
        /// Creates a call to WriteBoolean with value.
        /// </summary>
        /// <param name = "processor"></param>
        /// <param name = "writerParameterDef"></param>
        /// <param name = "value"></param>
        internal void CreateWriteBool(ILProcessor processor, ParameterDefinition writerParameterDef, bool value)
        {
            MethodReference writeBoolMethodRef = GetWriteMethodReference(GetClass<GeneralHelper>().GetTypeReference(typeof(bool)));
            processor.Emit(OpCodes.Ldarg, writerParameterDef);
            int intValue = value ? 1 : 0;
            processor.Emit(OpCodes.Ldc_I4, intValue);
            processor.Emit(writeBoolMethodRef.GetCallOpCode(Session), writeBoolMethodRef);
        }

        /// <summary>
        /// Returns if a type should use a declared/custom serializer globally.
        /// </summary>
        public bool DeclaresUseGlobalCustomSerializer(TypeReference dataTypeRef)
        {
            return dataTypeRef.CachedResolve(Session).HasCustomAttribute<UseGlobalCustomSerializerAttribute>();
        }

        /// <summary>
        /// Creates a Write call on a PooledWriter variable for parameterDef.
        /// EG: writer.WriteBool(xxxxx);
        /// </summary>
        internal List<Instruction> CreateWriteInstructions(MethodDefinition methodDef, object pooledWriterDef, ParameterDefinition valueParameterDef, MethodReference writeMr)
        {
            List<Instruction> insts = new();
            ILProcessor processor = methodDef.Body.GetILProcessor();

            if (writeMr != null)
            {
                bool declaresUseGlobalSerializer = DeclaresUseGlobalCustomSerializer(valueParameterDef.ParameterType);

                if (pooledWriterDef is VariableDefinition)
                {
                    insts.Add(processor.Create(OpCodes.Ldloc, (VariableDefinition)pooledWriterDef));
                }
                else if (pooledWriterDef is ParameterDefinition)
                {
                    insts.Add(processor.Create(OpCodes.Ldarg, (ParameterDefinition)pooledWriterDef));
                }
                else
                {
                    LogError($"{pooledWriterDef.GetType().FullName} is not a valid writerDef. Type must be VariableDefinition or ParameterDefinition.");
                    return new();
                }

                insts.Add(processor.Create(OpCodes.Ldarg, valueParameterDef));

                TypeReference valueTr = valueParameterDef.ParameterType;
                /* If generic then find write class for
                 * data type. Currently we only support one generic
                 * for this. */
                if (valueTr.IsGenericInstance)
                {
                    GenericInstanceType git = (GenericInstanceType)valueTr;
                    TypeReference genericTr = git.GenericArguments[0];
                    writeMr = writeMr.GetMethodReference(Session, genericTr);
                }

                if (declaresUseGlobalSerializer)
                {
                    // Switch out to use WriteUnpacked<T> instead.
                    writeMr = GetClass<WriterImports>().Writer_Write_MethodRef.GetMethodReference(Session, valueTr);
                }

                insts.Add(processor.Create(OpCodes.Call, writeMr));
                return insts;
            }
            else
            {
                LogError($"Writer not found for {valueParameterDef.ParameterType.FullName}.");
                return new();
            }
        }

        /// <summary>
        /// Creates a Write call on a PooledWriter variable for parameterDef.
        /// EG: writer.WriteBool(xxxxx);
        /// </summary>
        internal void CreateWrite(MethodDefinition methodDef, object writerDef, ParameterDefinition valuePd, MethodReference writeMr)
        {
            List<Instruction> insts = CreateWriteInstructions(methodDef, writerDef, valuePd, writeMr);
            ILProcessor processor = methodDef.Body.GetILProcessor();
            processor.Add(insts);
        }

        /// <summary>
        /// Creates a Write call to a writer.
        /// EG: StaticClass.WriteBool(xxxxx);
        /// </summary>
        /// <param name = "processor"></param>
        /// <param name = "memberValueFd"></param>
        internal void CreateWrite(MethodDefinition writerMd, ParameterDefinition encasingValuePd, FieldDefinition memberValueFd, MethodReference writeMr)
        {
            if (writeMr != null)
            {
                bool declaresUseGlobalSerializer = DeclaresUseGlobalCustomSerializer(memberValueFd.FieldType) || DeclaresUseGlobalCustomSerializer(encasingValuePd.ParameterType);

                ILProcessor processor = writerMd.Body.GetILProcessor();
                ParameterDefinition writerPd = writerMd.Parameters[0];

                /* If generic then find write class for
                 * data type. Currently we only support one generic
                 * for this. */
                if (memberValueFd.FieldType.IsGenericInstance)
                {
                    GenericInstanceType git = (GenericInstanceType)memberValueFd.FieldType;
                    TypeReference genericTr = git.GenericArguments[0];
                    writeMr = writeMr.GetMethodReference(Session, genericTr);
                }

                FieldReference fieldRef = GetClass<GeneralHelper>().GetFieldReference(memberValueFd);
                processor.Emit(OpCodes.Ldarg, writerPd);
                processor.Emit(OpCodes.Ldarg, encasingValuePd);
                processor.Emit(OpCodes.Ldfld, fieldRef);

                /* If a generated write then instead of calling the
                 * generated write directly call writer.Write<T> of
                 * the type.
                 *
                 * This will reroute to the generic writer, which does add an
                 * extra step, but this also allows us to decide what writer
                 * to use. In GenericWriter we can check if a method being set
                 * as the writer is generated, and if so while another method
                 * had already been set then favor the other method.
                 *
                 * This will favor built-in and user created serializers. This has to be
                 * done because we cannot check if a user created serializer exist
                 * across assemblies, but at runtime we can make sure to favor the
                 * created one as described above. */
                // True if has Write prefix for generated writers.
                if (declaresUseGlobalSerializer)
                {
                    // Switch out to use WriteUnpacked<T> instead.
                    TypeReference genericTr = ImportReference(memberValueFd.FieldType);
                    writeMr = GetClass<WriterImports>().Writer_Write_MethodRef.GetMethodReference(Session, genericTr);
                }

                processor.Emit(OpCodes.Call, writeMr);
            }
            else
            {
                LogError($"Writer not found for {memberValueFd.FieldType.FullName}.");
            }
        }

        /// <summary>
        /// Creates a Write call to a writer.
        /// EG: StaticClass.WriteBool(xxxxx);
        /// </summary>
        /// <param name = "processor"></param>
        /// <param name = "propertyDef"></param>
        internal void CreateWrite(MethodDefinition writerMd, ParameterDefinition valuePd, MethodReference getMr, MethodReference writeMr)
        {
            TypeReference returnTr = ImportReference(getMr.ReturnType);

            if (writeMr != null)
            {
                ILProcessor processor = writerMd.Body.GetILProcessor();
                ParameterDefinition writerPd = writerMd.Parameters[0];

                /* If generic then find write class for
                 * data type. Currently we only support one generic
                 * for this. */
                if (returnTr.IsGenericInstance)
                {
                    GenericInstanceType git = (GenericInstanceType)returnTr;
                    TypeReference genericTr = git.GenericArguments[0];
                    writeMr = writeMr.GetMethodReference(Session, genericTr);
                }

                processor.Emit(OpCodes.Ldarg, writerPd);
                OpCode ldArgOC0 = valuePd.ParameterType.IsValueType ? OpCodes.Ldarga : OpCodes.Ldarg;
                processor.Emit(ldArgOC0, valuePd);
                processor.Emit(OpCodes.Call, getMr);
                processor.Emit(OpCodes.Call, writeMr);
            }
            else
            {
                LogError($"Writer not found for {returnTr.FullName}.");
            }
        }

        #region TypeReference writer generators.
        /// <summary>
        /// Generates a writer for objectTypeReference if one does not already exist.
        /// </summary>
        /// <param name = "objectTr"></param>
        /// <returns></returns>
        internal MethodReference CreateWriter(TypeReference objectTr, string typeTrace)
        {
            MethodReference methodRefResult = null;
            TypeDefinition objectTd;
            SerializerType serializerType = GetClass<GeneratorHelper>().GetSerializerType(objectTr, true, out objectTd, typeTrace);
            if (serializerType != SerializerType.Invalid)
            {
                // Array.
                if (serializerType == SerializerType.Array)
                    methodRefResult = CreateArrayWriterMethodReference(objectTr);
                // Enum.
                else if (serializerType == SerializerType.Enum)
                    methodRefResult = CreateEnumWriterMethodDefinition(objectTr);
                // Dictionary, List, ListCache
                else if (serializerType == SerializerType.Dictionary || serializerType == SerializerType.List || serializerType == SerializerType.HashSet)
                    methodRefResult = CreateGenericCollectionWriterMethodReference(objectTr, serializerType);
                // NetworkBehaviour.
                else if (serializerType == SerializerType.NetworkBehaviour)
                    methodRefResult = CreateNetworkBehaviourWriterMethodReference(objectTd);
                // Nullable type.
                else if (serializerType == SerializerType.Nullable)
                    methodRefResult = CreateNullableWriterMethodReference(objectTr, objectTd);
                // Class or struct.
                else if (serializerType == SerializerType.ClassOrStruct)
                    methodRefResult = CreateClassOrStructWriterMethodDefinition(objectTr);
            }

            // If was not created.
            if (methodRefResult == null)
                RemoveFromStaticWriters(objectTr);

            return methodRefResult;
        }

        /// <summary>
        /// Removes from static writers.
        /// </summary>
        private void RemoveFromStaticWriters(TypeReference tr)
        {
            GetClass<WriterProcessor>().RemoveWriterMethod(tr, false);
        }

        /// <summary>
        /// Adds to static writers.
        /// </summary>
        private void AddToStaticWriters(TypeReference tr, MethodReference mr)
        {
            GetClass<WriterProcessor>().AddWriterMethod(tr, mr.CachedResolve(Session), false, true);
        }

        /// <summary>
        /// Adds a writer for a NetworkBehaviour class type to WriterMethods.
        /// </summary>
        /// <param name = "classTypeRef"></param>
        private MethodReference CreateNetworkBehaviourWriterMethodReference(TypeReference objectTr)
        {
            ObjectHelper oh = GetClass<ObjectHelper>();

            objectTr = ImportReference(objectTr.Resolve());
            // All NetworkBehaviour types will simply WriteNetworkBehaviour/ReadNetworkBehaviour.
            // Create generated reader/writer class. This class holds all generated reader/writers.
            GetClass<GeneralHelper>().GetOrCreateClass(out _, GENERATED_TYPE_ATTRIBUTES, GENERATED_WRITERS_CLASS_NAME, null);

            MethodDefinition createdWriterMd = CreateStaticWriterStubMethodDefinition(objectTr);
            AddToStaticWriters(objectTr, createdWriterMd);

            ILProcessor processor = createdWriterMd.Body.GetILProcessor();

            MethodReference writeMethodRef = GetClass<WriterProcessor>().GetOrCreateWriteMethodReference(oh.NetworkBehaviour_TypeRef, string.Empty);
            // Get parameters for method.
            ParameterDefinition writerParameterDef = createdWriterMd.Parameters[0];
            ParameterDefinition classParameterDef = createdWriterMd.Parameters[1];

            // Load parameters as arguments.
            processor.Emit(OpCodes.Ldarg, writerParameterDef);
            processor.Emit(OpCodes.Ldarg, classParameterDef);
            // writer.WriteNetworkBehaviour(arg1);
            processor.Emit(OpCodes.Call, writeMethodRef);

            processor.Emit(OpCodes.Ret);

            return ImportReference(createdWriterMd);
        }

        /// <summary>
        /// Gets the length of a collection and writes the value to a variable.
        /// </summary>
        private void CreateCollectionLength(ILProcessor processor, ParameterDefinition collectionParameterDef, VariableDefinition storeVariableDef)
        {
            processor.Emit(OpCodes.Ldarg, collectionParameterDef);
            processor.Emit(OpCodes.Ldlen);
            processor.Emit(OpCodes.Conv_I4);
            processor.Emit(OpCodes.Stloc, storeVariableDef);
        }

        /// <summary>
        /// Creates a writer for a class or struct of objectTypeRef.
        /// </summary>
        /// <param name = "objectTr"></param>
        /// <returns></returns>
        private MethodReference CreateNullableWriterMethodReference(TypeReference objectTr, TypeDefinition objectTd)
        {
            WriterProcessor wh = GetClass<WriterProcessor>();

            GenericInstanceType objectGit = objectTr as GenericInstanceType;
            TypeReference valueTr = objectGit.GenericArguments[0];

            // Get the writer for the value.
            MethodReference valueWriterMr = wh.GetOrCreateWriteMethodReference(valueTr, TypeReferenceTraceText(objectTr));
            if (valueWriterMr == null)
                return null;


            MethodDefinition tmpMd;
            tmpMd = objectTd.GetMethod("get_Value");
            MethodReference genericGetValueMr = tmpMd.MakeHostInstanceGeneric(Session, objectGit);
            tmpMd = objectTd.GetMethod("get_HasValue");
            MethodReference genericHasValueMr = tmpMd.MakeHostInstanceGeneric(Session, objectGit);

            /* Stubs generate Method(Writer writer, T value). */
            MethodDefinition createdWriterMd = CreateStaticWriterStubMethodDefinition(objectTr);
            AddToStaticWriters(objectTr, createdWriterMd);

            ILProcessor processor = createdWriterMd.Body.GetILProcessor();

            // Value parameter.
            ParameterDefinition valuePd = createdWriterMd.Parameters[1];
            ParameterDefinition writerPd = createdWriterMd.Parameters[0];

            // Have to write a new ret on null because nullables use hasValue for null checks.
            Instruction afterNullRetInst = processor.Create(OpCodes.Nop);
            processor.Emit(OpCodes.Ldarga, valuePd);
            processor.Emit(OpCodes.Call, genericHasValueMr);
            processor.Emit(OpCodes.Brtrue_S, afterNullRetInst);
            wh.CreateWriteBool(processor, writerPd, true);
            processor.Emit(OpCodes.Ret);
            processor.Append(afterNullRetInst);

            // Code will only execute here and below if not null.
            wh.CreateWriteBool(processor, writerPd, false);

            processor.Emit(OpCodes.Ldarg, writerPd);
            processor.Emit(OpCodes.Ldarga, valuePd);
            processor.Emit(OpCodes.Call, genericGetValueMr);
            processor.Emit(OpCodes.Call, valueWriterMr);

            processor.Emit(OpCodes.Ret);
            return ImportReference(createdWriterMd);
        }

        /// <summary>
        /// Creates a writer for a class or struct of objectTypeRef.
        /// </summary>
        /// <param name = "objectTr"></param>
        /// <returns></returns>
        private MethodReference CreateClassOrStructWriterMethodDefinition(TypeReference objectTr)
        {
            WriterProcessor wh = GetClass<WriterProcessor>();

            /*Stubs generate Method(Writer writer, T value). */
            MethodDefinition createdWriterMd = CreateStaticWriterStubMethodDefinition(objectTr);
            AddToStaticWriters(objectTr, createdWriterMd);
            ILProcessor processor = createdWriterMd.Body.GetILProcessor();

            // If not a value type then add a null check.
            if (!objectTr.CachedResolve(Session).IsValueType)
            {
                ParameterDefinition writerPd = createdWriterMd.Parameters[0];
                wh.CreateRetOnNull(processor, writerPd, createdWriterMd.Parameters[1]);
                // Code will only execute here and below if not null.
                wh.CreateWriteBool(processor, writerPd, false);
            }

            // Write all fields for the class or struct.
            ParameterDefinition valueParameterDef = createdWriterMd.Parameters[1];
            if (!WriteFieldsAndProperties(createdWriterMd, valueParameterDef, objectTr))
                return null;

            processor.Emit(OpCodes.Ret);
            return ImportReference(createdWriterMd);
        }

        /// <summary>
        /// Find all fields in type and write them
        /// </summary>
        /// <param name = "objectTr"></param>
        /// <param name = "processor"></param>
        /// <returns>false if fail</returns>
        private bool WriteFieldsAndProperties(MethodDefinition generatedWriteMd, ParameterDefinition encasingValuePd, TypeReference objectTr)
        {
            WriterProcessor wh = GetClass<WriterProcessor>();

            // This probably isn't needed but I'm too afraid to remove it.
            if (objectTr.Module != Module)
                objectTr = ImportReference(objectTr.CachedResolve(Session));

            // Fields
            foreach (FieldDefinition fieldDef in objectTr.FindAllSerializableFields(Session)) // , WriterHelper.EXCLUDED_AUTO_SERIALIZER_TYPES))
            {
                TypeReference tr;
                if (fieldDef.FieldType.IsGenericInstance)
                {
                    GenericInstanceType genericTr = (GenericInstanceType)fieldDef.FieldType;
                    tr = genericTr.GenericArguments[0];
                }
                else
                {
                    tr = fieldDef.FieldType;
                }

                if (GetWriteMethod(fieldDef.FieldType, out MethodReference writeMr))
                    wh.CreateWrite(generatedWriteMd, encasingValuePd, fieldDef, writeMr);
            }

            // Properties.
            foreach (PropertyDefinition propertyDef in objectTr.FindAllSerializableProperties(Session, EXCLUDED_AUTO_SERIALIZER_TYPES, EXCLUDED_ASSEMBLY_PREFIXES))
            {
                if (GetWriteMethod(propertyDef.PropertyType, out MethodReference writerMr))
                {
                    MethodReference getMr = Module.ImportReference(propertyDef.GetMethod);
                    wh.CreateWrite(generatedWriteMd, encasingValuePd, getMr, writerMr);
                }
            }

            // Gets or creates writer method and outputs it. Returns true if method is found or created.
            bool GetWriteMethod(TypeReference tr, out MethodReference writeMr)
            {
                tr = ImportReference(tr);
                writeMr = wh.GetOrCreateWriteMethodReference(tr, TypeReferenceTraceText(objectTr));
                return writeMr != null;
            }

            return true;
        }

        /// <summary>
        /// Creates a writer for an enum.
        /// </summary>
        /// <param name = "objectTr"></param>
        /// <returns></returns>
        private MethodReference CreateEnumWriterMethodDefinition(TypeReference objectTr)
        {
            WriterProcessor wh = GetClass<WriterProcessor>();

            MethodDefinition createdWriterMd = CreateStaticWriterStubMethodDefinition(objectTr);
            AddToStaticWriters(objectTr, createdWriterMd);

            ILProcessor processor = createdWriterMd.Body.GetILProcessor();

            // Element type for enum. EG: byte int ect
            TypeReference underlyingTypeRef = objectTr.CachedResolve(Session).GetEnumUnderlyingTypeReference();
            // Method to write that type.
            MethodReference underlyingWriterMethodRef = wh.GetOrCreateWriteMethodReference(underlyingTypeRef, TypeReferenceTraceText(objectTr));
            if (underlyingWriterMethodRef == null)
                return null;

            ParameterDefinition writerParameterDef = createdWriterMd.Parameters[0];
            ParameterDefinition valueParameterDef = createdWriterMd.Parameters[1];
            // Push writer and value into call.
            processor.Emit(OpCodes.Ldarg, writerParameterDef);
            processor.Emit(OpCodes.Ldarg, valueParameterDef);

            // writer.WriteXXX(value)
            processor.Emit(OpCodes.Call, underlyingWriterMethodRef);

            processor.Emit(OpCodes.Ret);
            return ImportReference(createdWriterMd);
        }

        /// <summary>
        /// Calls an instanced writer from a static writer.
        /// </summary>
        private void CallInstancedWriter(MethodDefinition staticWriterMd, MethodReference instancedWriterMr)
        {
            ParameterDefinition writerPd = staticWriterMd.Parameters[0];
            ParameterDefinition valuePd = staticWriterMd.Parameters[1];
            ILProcessor processor = staticWriterMd.Body.GetILProcessor();
            processor.Emit(OpCodes.Ldarg, writerPd);
            processor.Emit(OpCodes.Ldarg, valuePd);
            processor.Emit(instancedWriterMr.GetCallOpCode(Session), instancedWriterMr);
            processor.Emit(OpCodes.Ret);
        }

        /// <summary>
        /// Creates a writer for an array.
        /// </summary>
        private MethodReference CreateArrayWriterMethodReference(TypeReference objectTr)
        {
            WriterImports wi = GetClass<WriterImports>();
            TypeReference valueTr = objectTr.GetElementType();

            //Write not found.
            if (GetOrCreateWriteMethodReference(valueTr, TypeReferenceTraceText(objectTr)) == null)
                return null;

            MethodDefinition createdMd = CreateStaticWriterStubMethodDefinition(objectTr);
            AddToStaticWriters(objectTr, createdMd);

            //Find instanced writer to use.
            MethodReference instancedWriteMr = wi.Writer_WriteArray_MethodRef;
            //Make generic.
            GenericInstanceMethod writeGim = instancedWriteMr.MakeGenericMethod(new TypeReference[] { valueTr });
            CallInstancedWriter(createdMd, writeGim);

            return ImportReference(createdMd);
        }

        /// <summary>
        /// Creates a writer for a variety of generic collections.
        /// </summary>
        private MethodReference CreateGenericCollectionWriterMethodReference(TypeReference objectTr, SerializerType st)
        {
            WriterImports wi = GetClass<WriterImports>();
            //Make value field generic.
            GenericInstanceType genericInstance = (GenericInstanceType)objectTr;
            ImportReference(genericInstance);
            TypeReference valueTr = genericInstance.GenericArguments[0];

            List<TypeReference> genericArguments = new();
            //Make sure all arguments have writers.
            foreach (TypeReference gaTr in genericInstance.GenericArguments)
            {
                MethodReference mr = GetOrCreateWriteMethodReference(gaTr, TypeReferenceTraceText(objectTr));
                //Writer not found.
                if (mr == null)
                {
                    LogError($"Writer could not be found or created for type {gaTr.FullName}.");
                    return null;
                }

                genericArguments.Add(gaTr);
            }

            MethodReference valueWriteMr = GetOrCreateWriteMethodReference(valueTr, TypeReferenceTraceText(objectTr));
            if (valueWriteMr == null)
                return null;

            MethodDefinition createdMd = CreateStaticWriterStubMethodDefinition(objectTr);
            AddToStaticWriters(objectTr, createdMd);

            //Find instanced writer to use.
            MethodReference instancedWriteMr;
            if (st == SerializerType.Dictionary)
                instancedWriteMr = wi.Writer_WriteDictionary_MethodRef;
            else if (st == SerializerType.List)
                instancedWriteMr = wi.Writer_WriteList_MethodRef;
            else if (st == SerializerType.HashSet)
                instancedWriteMr = wi.Writer_WriteHashSet_MethodRef;
            else
                instancedWriteMr = null;

            //Not found.
            if (instancedWriteMr == null)
            {
                LogError($"Instanced writer not found for SerializerType {st} on object {objectTr.Name}.");
                return null;
            }

            //Make generic.
            GenericInstanceMethod writeGim = instancedWriteMr.MakeGenericMethod(genericArguments.ToArray());
            CallInstancedWriter(createdMd, writeGim);

            return ImportReference(createdMd);
        }

        /// <summary>
        /// Creates a method definition stub for objectTypeRef.
        /// </summary>
        /// <param name = "objectTypeRef"></param>
        /// <returns></returns>
        public MethodDefinition CreateStaticWriterStubMethodDefinition(TypeReference objectTypeRef, string nameExtension = GENERATED_WRITER_NAMESPACE)
        {
            string methodName = $"{UtilityConstants.GeneratedWriterPrefix}{objectTypeRef.FullName}{nameExtension}";
            // create new writer for this type
            TypeDefinition writerTypeDef = GetClass<GeneralHelper>().GetOrCreateClass(out _, GENERATED_TYPE_ATTRIBUTES, GENERATED_WRITERS_CLASS_NAME, null);

            MethodDefinition writerMethodDef = writerTypeDef.AddMethod(methodName, MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig);

            GetClass<GeneralHelper>().CreateParameter(writerMethodDef, GetClass<WriterImports>().Writer_TypeRef, "writer");
            GetClass<GeneralHelper>().CreateParameter(writerMethodDef, objectTypeRef, "value");
            GetClass<GeneralHelper>().MakeExtensionMethod(writerMethodDef);
            writerMethodDef.Body.InitLocals = true;

            return writerMethodDef;
        }
        #endregion
    }
}