using System;
using System.Collections.Generic;
using System.Reflection;
using System.Xml;
using System.IO;
using System.Linq;
using System.Text;

namespace UnityEditor.Build.Pipeline.Utilities
{
    /// <summary>
    /// This can be used to create a LinkXml for your build.  This will ensure that the desired runtime types are packed into the build.
    /// </summary>
    public class LinkXmlGenerator
    {
        Dictionary<Type, Type> m_TypeConversion = new Dictionary<Type, Type>();
        HashSet<Type> m_Types = new HashSet<Type>();
        HashSet<Assembly> m_Assemblies = new HashSet<Assembly>();

        /// <summary>
        /// Obsolete, no longer does anything.
        /// </summary>
        [Obsolete] protected Dictionary<string, HashSet<string>> serializedClassesPerAssembly = null;
        Dictionary<string, HashSet<string>> m_SerializedClassesPerAssembly = new Dictionary<string, HashSet<string>>();

        /// <summary>
        /// Constructs and returns a LinkXmlGenerator object that contains default UnityEditor to UnityEngine type conversions.
        /// </summary>
        /// <returns>LinkXmlGenerator object with the default UnityEngine type conversions.</returns>
        public static LinkXmlGenerator CreateDefault()
        {
            var linker = new LinkXmlGenerator();
            var types = GetEditorTypeConversions();
            foreach (var pair in types)
                linker.SetTypeConversion(pair.Key, pair.Value);
            return linker;
        }

        /// <summary>
        /// Returns the set of UnityEditor types that have valid runtime direct mappings.
        /// </summary>
        /// <returns>Array of KeyValuePairs containing the editor type and it's equivalent runtime type.</returns>
        public static KeyValuePair<Type, Type>[] GetEditorTypeConversions()
        {
            var editor = Assembly.GetAssembly(typeof(UnityEditor.BuildPipeline));
            return new[]
            {
                new KeyValuePair<Type, Type>(typeof(UnityEditor.Animations.AnimatorController), typeof(UnityEngine.RuntimeAnimatorController)),
                new KeyValuePair<Type, Type>(editor.GetType("UnityEditor.Audio.AudioMixerController"), typeof(UnityEngine.Audio.AudioMixer)),
                new KeyValuePair<Type, Type>(editor.GetType("UnityEditor.Audio.AudioMixerGroupController"), typeof(UnityEngine.Audio.AudioMixerGroup)),
                new KeyValuePair<Type, Type>(typeof(UnityEditor.MonoScript), typeof(UnityEngine.Object)),
            };
        }

        /// <summary>
        /// Add runtime assembly to the LinkXml Generator.
        /// </summary>
        /// <param name="assemblies">The desired runtime assemblies.</param>
        public void AddAssemblies(params Assembly[] assemblies)
        {
            if (assemblies == null)
                return;
            foreach (var a in assemblies)
                AddAssemblyInternal(a);
        }

        /// <summary>
        /// Add runtime assembly to the LinkXml Generator.
        /// </summary>
        /// <param name="assemblies">The desired runtime assemblies.</param>
        public void AddAssemblies(IEnumerable<Assembly> assemblies)
        {
            if (assemblies == null)
                return;
            foreach (var a in assemblies)
                AddAssemblyInternal(a);
        }

        /// <summary>
        /// Add runtime type to the LinkXml Generator.
        /// </summary>
        /// <param name="types">The desired runtime types.</param>
        public void AddTypes(params Type[] types)
        {
            if (types == null)
                return;
            foreach (var t in types)
                AddTypeInternal(t);
        }

        /// <summary>
        /// Add runtime type to the LinkXml Generator.
        /// </summary>
        /// <param name="types">The desired runtime types.</param>
        public void AddTypes(IEnumerable<Type> types)
        {
            if (types == null)
                return;
            foreach (var t in types)
                AddTypeInternal(t);
        }

        /// <summary>
        /// Add SerializedReference class type from fully qualified name to the Generator, those will end up in PreservedTypes.xml
        /// </summary>
        /// <param name="serializedRefTypes">The SerializeReference instance fully qualified name we want to preserve.</param>
        public void AddSerializedClass(IEnumerable<string> serializedRefTypes)
        {
            if (serializedRefTypes == null)
                return;
            foreach (var t in serializedRefTypes)
            {
                var indexOfAssembly = t.IndexOf(':');
                if (indexOfAssembly != -1)
                    AddSerializedClassInternal(t.Substring(0, indexOfAssembly), t.Substring(indexOfAssembly + 1, t.Length - (indexOfAssembly + 1)));
            }
        }

        private void AddTypeInternal(Type t)
        {
            if (t == null)
                return;

            Type convertedType;
            if (m_TypeConversion.TryGetValue(t, out convertedType))
                m_Types.Add(convertedType);
            else
                m_Types.Add(t);
        }

        private void AddSerializedClassInternal(string assemblyName, string classWithNameSpace)
        {
            if (string.IsNullOrEmpty(assemblyName))
                return;

            if (string.IsNullOrEmpty(classWithNameSpace))
                return;

            if (!m_SerializedClassesPerAssembly.TryGetValue(assemblyName, out HashSet<string> types))
                m_SerializedClassesPerAssembly[assemblyName] = types = new HashSet<string>();

            types.Add(classWithNameSpace);
        }

        private void AddAssemblyInternal(Assembly a)
        {
            if (a == null)
                return;

            m_Assemblies.Add(a);
        }

        /// <summary>
        /// Setup runtime type conversion
        /// </summary>
        /// <param name="a">Convert from type.</param>
        /// <param name="b">Convert to type.</param>
        public void SetTypeConversion(Type a, Type b)
        {
            m_TypeConversion[a] = b;
        }

        /// <summary>
        /// Save the LinkXml to the specified path.
        /// </summary>
        /// <param name="path">The path to save the linker xml file.</param>
        public void Save(string path)
        {
            var assemblyMap = new Dictionary<Assembly, List<Type>>();
            foreach (var a in m_Assemblies)
            {
                if (!assemblyMap.TryGetValue(a, out _))
                    assemblyMap.Add(a, new List<Type>());
            }
            foreach (var t in m_Types)
            {
                var a = t.Assembly;
                List<Type> types;
                if (!assemblyMap.TryGetValue(a, out types))
                    assemblyMap.Add(a, types = new List<Type>());
                types.Add(t);
            }
            XmlDocument doc = new XmlDocument();
            var linker = doc.AppendChild(doc.CreateElement("linker"));
            foreach (var k in assemblyMap.OrderBy(a => a.Key.FullName))
            {
                var assembly = linker.AppendChild(doc.CreateElement("assembly"));
                var attr = doc.CreateAttribute("fullname");
                attr.Value = k.Key.FullName;
                if (assembly.Attributes != null)
                {
                    assembly.Attributes.Append(attr);

                    if (m_Assemblies.Contains(k.Key))
                    {
                        var preserveAssembly = doc.CreateAttribute("preserve");
                        preserveAssembly.Value = "all";
                        assembly.Attributes.Append(preserveAssembly);
                    }

                    foreach (var t in k.Value.OrderBy(t => t.FullName))
                    {
                        var typeEl = assembly.AppendChild(doc.CreateElement("type"));
                        var tattr = doc.CreateAttribute("fullname");
                        tattr.Value = t.FullName;
                        if (typeEl.Attributes != null)
                        {
                            typeEl.Attributes.Append(tattr);
                            var pattr = doc.CreateAttribute("preserve");
                            pattr.Value = "all";
                            typeEl.Attributes.Append(pattr);
                        }
                    }

                    //Add serialize reference classes which are contained in the current assembly
                    var assemblyName = k.Key.GetName().Name;
                    if (m_SerializedClassesPerAssembly.ContainsKey(assemblyName))
                    {
                        //Add content for this
                        foreach (var t in m_SerializedClassesPerAssembly[assemblyName])
                        {
                            var typeEl = assembly.AppendChild(doc.CreateElement("type"));
                            var tattr = doc.CreateAttribute("fullname");
                            tattr.Value = t;
                            if (typeEl.Attributes != null)
                            {
                                typeEl.Attributes.Append(tattr);
                                var pattr = doc.CreateAttribute("preserve");
                                pattr.Value = "nothing";
                                typeEl.Attributes.Append(pattr);
                                var sattr = doc.CreateAttribute("serialized");
                                sattr.Value = "true";
                                typeEl.Attributes.Append(sattr);
                            }
                        }
                        m_SerializedClassesPerAssembly.Remove(assemblyName);
                    }
                }
            }

            //Add serialize reference classes which are contained in other assemblies not yet removed.
            foreach (var k in m_SerializedClassesPerAssembly.OrderBy(a => a.Key))
            {
                var assembly = linker.AppendChild(doc.CreateElement("assembly"));
                var attr = doc.CreateAttribute("fullname");
                attr.Value = k.Key;
                if (assembly.Attributes != null)
                {
                    assembly.Attributes.Append(attr);
                    //Add content for this
                    foreach (var t in k.Value.OrderBy(t => t))
                    {
                        var typeEl = assembly.AppendChild(doc.CreateElement("type"));
                        var tattr = doc.CreateAttribute("fullname");
                        tattr.Value = t;
                        if (typeEl.Attributes != null)
                        {
                            typeEl.Attributes.Append(tattr);
                            var pattr = doc.CreateAttribute("preserve");
                            pattr.Value = "nothing";
                            typeEl.Attributes.Append(pattr);
                            var sattr = doc.CreateAttribute("serialized");
                            sattr.Value = "true";
                            typeEl.Attributes.Append(sattr);
                        }
                    }
                }
            }
            doc.Save(path);
        }
    }
}