using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using Unity.VisualScripting;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
using UnityEditor.Callbacks;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;

internal class LinkerCreator : IPreprocessBuildWithReport
{
    private static string linkerPath => Path.Combine(BoltCore.Paths.persistentGenerated, "link.xml");

    public int callbackOrder { get; }

    private static ManagedStrippingLevel GetManagedStrippingLevel(BuildTargetGroup buildTarget)
    {
#if UNITY_2023_1_OR_NEWER
        var namedBuildTarget = UnityEditor.Build.NamedBuildTarget.FromBuildTargetGroup(buildTarget);
        return PlayerSettings.GetManagedStrippingLevel(namedBuildTarget);
#else
        return PlayerSettings.GetManagedStrippingLevel(buildTarget);
#endif
    }

    public void OnPreprocessBuild(BuildReport report)
    {
        if (VSUsageUtility.isVisualScriptingUsed)
        {
            try
            {
                if (GetManagedStrippingLevel(EditorUserBuildSettings.selectedBuildTargetGroup) !=
                    ManagedStrippingLevel.Disabled)
                {
                    GenerateLinker();
                }
            }
            catch (Exception ex)
            {
                Debug.LogException(ex);

                DeleteLinker();
            }
        }
    }

    [PostProcessBuild]
    private static void OnPostprocessBuild(BuildTarget buildTarget, string path)
    {
        if (VSUsageUtility.isVisualScriptingUsed)
        {
            DeleteLinker();
        }
    }

    private static void DeleteLinker()
    {
        PathUtility.DeleteProjectFileIfExists(linkerPath, true);
    }

    // Automatically generates the link.xml file to prevent stripping.
    // Currently only used for plugin assemblies, because blanket preserving
    // all setting assemblies sometimes causes the IL2CPP process to fail.
    // For settings assemblies, the AOT stubs are good enough to fool
    // the static code analysis without needing this full coverage.
    // https://docs.unity3d.com/Manual/iphone-playerSizeOptimization.html
    // However, for FullSerializer, we need to preserve our custom assemblies.
    // This is mostly because IL2CPP will attempt to transform non-public
    // property setters used in deserialization into read-only accessors
    // that return false on PropertyInfo.CanWrite, but only in stripped builds.
    // Therefore, in stripped builds, FS will skip properties that should be
    // deserialized without any error (and that took hours of debugging to figure out).
    public static void GenerateLinker()
    {
        var linker = new XDocument();

        var linkerNode = new XElement("linker");

        if (!PluginContainer.initialized)
            PluginContainer.Initialize();

        foreach (var pluginAssembly in PluginContainer.plugins
                     .SelectMany(plugin => plugin.GetType()
                         .GetAttributes<PluginRuntimeAssemblyAttribute>()
                         .Select(a => a.assemblyName))
                     .Distinct())
        {
            var assemblyNode = new XElement("assembly");
            var fullnameAttribute = new XAttribute("fullname", pluginAssembly);
            var preserveAttribute = new XAttribute("preserve", "all");
            assemblyNode.Add(fullnameAttribute);
            assemblyNode.Add(preserveAttribute);
            linkerNode.Add(assemblyNode);
        }

        linker.Add(linkerNode);

        AddCustomNodesToLinker(linkerNode, PluginContainer.plugins);

        PathUtility.CreateDirectoryIfNeeded(BoltCore.Paths.transientGenerated);

        PathUtility.DeleteProjectFileIfExists(linkerPath, true);

        // Using ToString instead of Save to omit the <?xml> declaration,
        // which doesn't appear in the Unity documentation page for the linker.
        File.WriteAllText(linkerPath, linker.ToString());
    }

    private static void AddCustomNodesToLinker(XElement linkerNode, IEnumerable<Plugin> plugins)
    {
        var types = FindAllCustomTypes();

        var customTypes = types.Where(t => !plugins.Any(p => t.Assembly == p.runtimeAssembly));

        foreach (var type in customTypes)
        {
            var userAssembly = type.Assembly.GetName().Name;

            var assemblyNode = new XElement("assembly");
            var fullnameAttribute = new XAttribute("fullname", userAssembly);
            var preserveAttribute = new XAttribute("preserve", "all");

            assemblyNode.Add(fullnameAttribute);

            var customType = new XElement("type");
            var fullnameType = new XAttribute("fullname", type.FullName);

            customType.Add(fullnameType);
            customType.Add(preserveAttribute);

            assemblyNode.Add(customType);

            linkerNode.Add(assemblyNode);
        }
    }

    private static void ProcessSubGraphs(HashSet<Type> types, SubgraphUnit subgraph)
    {
        foreach (var unit in subgraph.nest.graph.units)
        {
            AddTypeToHashSet(types, unit);
        }
    }

    private static void AddTypeToHashSet(HashSet<Type> types, IUnit unit)
    {
        if (unit.GetType() == typeof(SubgraphUnit))
        {
            ProcessSubGraphs(types, (SubgraphUnit)unit);
        }
        else
        {
            types.Add(unit.GetType());
        }
    }

    private static HashSet<Type> FindGraphsOnAssets()
    {
        var scriptGraphAssets = AssetUtility.GetAllAssetsOfType<ScriptGraphAsset>();

        var types = new HashSet<Type>();

        var index = 0;
        var total = scriptGraphAssets.Count();

        foreach (var scriptGraphAsset in scriptGraphAssets)
        {
            if (EditorUtility.DisplayCancelableProgressBar($"Processing on assets {index}/{total}",
                    $"Asset {scriptGraphAsset.name}", (float)index / (float)total))
            {
                break;
            }

            index++;

            if (scriptGraphAsset.graph != null)
            {
                foreach (var unit in scriptGraphAsset.graph.units)
                {
                    AddTypeToHashSet(types, unit);
                }
            }
        }

        EditorUtility.ClearProgressBar();

        return types;
    }

    private static HashSet<Type> FindGraphsOnScenes(bool includeGraphAssets)
    {
        var activeScenePath = SceneManager.GetActiveScene().path;
        var scenePaths = EditorBuildSettings.scenes.Select(s => s.path).ToArray();

        var index = 0;
        var total = scenePaths.Count();
        var types = new HashSet<Type>();

        foreach (var scenePath in scenePaths)
        {
            index++;

            if (EditorUtility.DisplayCancelableProgressBar($"Processing scenes {index} / {total}", $"Scene {scenePath}",
                    (float)index / (float)total))
            {
                break;
            }

            if (!string.IsNullOrEmpty(scenePath))
            {
                EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single);

                var scriptMachines = UnityObjectUtility.FindObjectsOfTypeIncludingInactive<ScriptMachine>();

                foreach (var scriptMachine in scriptMachines)
                {
                    if (scriptMachine.nest != null &&
                        (scriptMachine.nest.source == GraphSource.Macro && includeGraphAssets) ||
                        scriptMachine.nest.source == GraphSource.Embed)
                    {
                        foreach (var unit in scriptMachine.graph.units)
                        {
                            AddTypeToHashSet(types, unit);
                        }
                    }
                }
            }
        }

        if (!string.IsNullOrEmpty(activeScenePath))
        {
            EditorSceneManager.OpenScene(activeScenePath);
        }

        GC.Collect();

        EditorUtility.ClearProgressBar();

        return types;
    }

    private static HashSet<Type> FindGraphsOnPrefabs(bool includeGraphAssets)
    {
        var types = new HashSet<Type>();

        var files = System.IO.Directory.GetFiles(Application.dataPath, "*.prefab",
            System.IO.SearchOption.AllDirectories);

        var index = 0;
        var total = files.Count();

        var currentScene = EditorSceneManager.GetActiveScene();
        var scenePath = currentScene.path;

        EditorSceneManager.NewScene(NewSceneSetup.EmptyScene);

        foreach (var file in files)
        {
            index++;

            if (EditorUtility.DisplayCancelableProgressBar($"Processing prefabs {index} / {total}", $"Prefab {file}",
                    (float)index / (float)total))
            {
                break;
            }

            var prefabPath = file.Replace(Application.dataPath, "Assets");

            var prefab = UnityEditor.AssetDatabase.LoadAssetAtPath(prefabPath, typeof(GameObject)) as GameObject;

            if (prefab != null)
            {
                FindGraphInPrefab(types, prefab, includeGraphAssets);
                prefab = null;

                EditorUtility.UnloadUnusedAssetsImmediate(true);
            }
        }

        EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single);

        EditorUtility.UnloadUnusedAssetsImmediate(true);

        GC.Collect();

        EditorUtility.ClearProgressBar();

        return types;
    }

    private static void FindGraphInPrefab(HashSet<Type> types, GameObject gameObject, bool includeGraphAssets)
    {
        var scriptMachines = gameObject.GetComponents<ScriptMachine>();

        foreach (var scriptMachine in scriptMachines)
        {
            if (scriptMachine.nest != null &&
                (scriptMachine.nest.source == GraphSource.Macro && includeGraphAssets) ||
                scriptMachine.nest.source == GraphSource.Embed)
            {
                foreach (var unit in scriptMachine.graph.units)
                {
                    AddTypeToHashSet(types, unit);
                }
            }
        }

        foreach (Transform child in gameObject.transform)
        {
            FindGraphInPrefab(types, child.gameObject, includeGraphAssets);
        }
    }

    private static HashSet<Type> FindAllCustomTypes()
    {
        var types = new HashSet<Type>();

        var settings = (List<bool>)BoltCore.Configuration.GetMetadata("LinkerPropertyProviderSettings").value;

        if (settings[(int)BoltCoreConfiguration.LinkerScanTarget.GraphAssets])
        {
            types.AddRange(FindGraphsOnAssets());
        }

        var includeGraphAssets = !settings[(int)BoltCoreConfiguration.LinkerScanTarget.GraphAssets];

        if (settings[(int)BoltCoreConfiguration.LinkerScanTarget.EmbeddedSceneGraphs])
        {
            types.AddRange(FindGraphsOnScenes(includeGraphAssets));
        }

        if (settings[(int)BoltCoreConfiguration.LinkerScanTarget.EmbeddedPrefabGraphs])
        {
            types.AddRange(FindGraphsOnPrefabs(includeGraphAssets));
        }

        return types;
    }
}