using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;

namespace Evo.UI.Migration
{
    internal enum InstalledVersion { None, V1, V2, Mixed }

    /// <summary>
    /// The migration without its window. This part finds the installed Evo UI version and keeps file access inside Assets.
    /// Applying an export is in MigrationEngine.Apply.cs, removing the tool in MigrationEngine.Tool.cs, and the analysis in AnalysisRun.
    /// The tool's assembly has no Evo references, so it keeps working while v1 is removed and v2 isn't imported yet.
    /// </summary>
#if UNITY_6000_5_OR_NEWER
    [Unity.Scripting.LifecycleManagement.NoAutoStaticsCleanup]
#endif
    internal static partial class MigrationEngine
    {
        /// <summary>
        /// Where the animation preset copies that migrated assets need are written, outside the removable tool folder.
        /// </summary>
        public const string PresetsFolder = "Assets/Evo UI Migrated Presets";

        /// <summary>
        /// Where the v1 sprites, fonts and prefabs that v2 doesn't ship are kept, so the assets that use them still find them.
        /// </summary>
        public const string KeptFolder = "Assets/Evo UI v1 Assets";

        static readonly string[] MigratedExtensions = { ".cs", ".prefab", ".unity", ".asset", ".anim", ".controller", ".overrideController" };

        // Other Evo products share Evo/Shared with Evo UI, and their v1 releases don't work with Evo UI v2
        static readonly (string name, string assembly)[] RelatedAssemblies =
        {
            ("Evo Localization", "Evo.Localization"),
            ("Evo Loader", "Evo.Loader")
        };

        #region Paths
        /// <summary>
        /// The project folder, which holds Assets and Library.
        /// </summary>
        public static string Root => Path.GetFullPath(Path.Combine(Application.dataPath, ".."));

        /// <summary>
        /// Library/EvoUIMigration: analysis runs, backups, and the automatic migration's state.
        /// </summary>
        public static string StateDirectory => Path.Combine(Root, "Library/EvoUIMigration");

        /// <summary>
        /// The tool's folder, such as "Assets/Evo UI - Migration Tool", found through the window's script so the folder can be moved.
        /// </summary>
        public static string ToolRoot => AssetDatabase.FindAssets("MigrationWindow t:MonoScript")
            .Select(AssetDatabase.GUIDToAssetPath)
            .Where(path => path.EndsWith("/MigrationWindow.cs", StringComparison.Ordinal))
            .Select(path => Path.GetDirectoryName(Path.GetDirectoryName(path)).Replace('\\', '/'))
            .FirstOrDefault();

        static string LastExportRecord => Path.Combine(StateDirectory, "last-export.txt");

        /// <summary>
        /// The most recently saved export, if the file still exists, so v2 can offer it without a file dialog.
        /// </summary>
        public static string LastExport
        {
            get
            {
                if (!File.Exists(LastExportRecord))
                    return null;

                string path = File.ReadAllText(LastExportRecord).Trim();
                return File.Exists(path) ? path : null;
            }
        }

        public static void RememberExport(string path)
        {
            Directory.CreateDirectory(StateDirectory);
            File.WriteAllText(LastExportRecord, Path.GetFullPath(path));
        }
        #endregion

        #region Versions
        /// <summary>
        /// Which Evo UI is installed, found by marker scripts in its source. Source works even while user scripts
        /// don't compile, as they don't in between removing v1 and importing v2.
        /// </summary>
        public static InstalledVersion DetectVersion()
        {
            bool v1 = HasV1;
            bool v2 = HasV2;
            if (v1 && v2)
                return InstalledVersion.Mixed;

            return v1 ? InstalledVersion.V1 : v2 ? InstalledVersion.V2 : InstalledVersion.None;
        }

        public static bool HasV1 => FindMarkerScripts("ProceduralRect").Any();
        public static bool HasV2 => FindMarkerScripts("ProceduralShape").Any();

        /// <summary>
        /// Evo UI scripts with a file name, checked on disk too, since the database can briefly list files that were just removed.
        /// </summary>
        public static IEnumerable<string> FindMarkerScripts(string name)
        {
            return AssetDatabase.FindAssets(name + " t:MonoScript")
                .Select(AssetDatabase.GUIDToAssetPath)
                .Where(path => Path.GetFileNameWithoutExtension(path) == name && File.Exists(path) && File.ReadAllText(path).Contains("namespace Evo.UI"));
        }
        #endregion

        #region Related assets
        /// <summary>
        /// Installed Evo products that need their v2 release with Evo UI v2, found by their assembly definitions.
        /// </summary>
        public static List<RelatedAsset> FindRelatedAssets()
        {
            string[] asmdefs = AssetDatabase.FindAssets("t:AssemblyDefinitionAsset").Select(AssetDatabase.GUIDToAssetPath).ToArray();
            var found = new List<RelatedAsset>();
            foreach (var (name, assembly) in RelatedAssemblies)
            {
                string asmdef = asmdefs.FirstOrDefault(path => Path.GetFileNameWithoutExtension(path) == assembly);
                if (asmdef != null)
                    found.Add(new RelatedAsset(name, Path.GetDirectoryName(asmdef).Replace('\\', '/')));
            }

            return found;
        }

        /// <summary>
        /// Names in a sentence, such as "Evo Localization and Evo Loader", optionally with their folders.
        /// </summary>
        public static string JoinNames(IReadOnlyList<RelatedAsset> assets, bool withFolders = false)
        {
            var names = assets.Select(asset => withFolders ? $"{asset.name} ({asset.folder})" : asset.name).ToList();
            if (names.Count <= 1)
                return names.FirstOrDefault() ?? "";

            return string.Join(", ", names.Take(names.Count - 1)) + " and " + names[^1];
        }
        #endregion

        #region Checks
        /// <summary>
        /// Throws unless every scene and asset is saved and Prefab Mode is closed, so nothing unsaved is lost or resaved over migrated files.
        /// </summary>
        public static void CheckSaved()
        {
            for (int i = 0; i < SceneManager.sceneCount; i++)
            {
                if (SceneManager.GetSceneAt(i).isDirty)
                    throw new InvalidOperationException("Save open scenes before migrating.");
            }

            if (PrefabStageUtility.GetCurrentPrefabStage() != null)
                throw new InvalidOperationException("Close Prefab Mode before migrating.");

            if (Resources.FindObjectsOfTypeAll<ScriptableObject>().Any(asset => EditorUtility.IsPersistent(asset) && EditorUtility.IsDirty(asset)))
                throw new InvalidOperationException("Save modified project assets before migrating.");
        }

        /// <summary>
        /// The full path of a project-relative path under Assets. Throws for anything that leaves Assets, including through linked folders.
        /// </summary>
        internal static string SafePath(string path)
        {
            if (string.IsNullOrEmpty(path) || Path.IsPathRooted(path) || !path.StartsWith("Assets/", StringComparison.Ordinal) || path.Split('/').Contains(".."))
                throw new InvalidDataException("Migration paths must stay under Assets.");

            string full = Path.GetFullPath(Path.Combine(Root, path));
            string assets = Application.dataPath.Replace('/', Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
            if (!full.StartsWith(assets, StringComparison.OrdinalIgnoreCase))
                throw new InvalidDataException("Unsafe migration path.");

            for (string parent = Path.GetDirectoryName(full); parent != null && parent.Length >= Application.dataPath.Length; parent = Path.GetDirectoryName(parent))
            {
                if (Directory.Exists(parent) && IsLink(parent))
                    throw new InvalidDataException("Migration through a linked folder is not supported: " + parent);
            }

            if (File.Exists(full) && IsLink(full))
                throw new InvalidDataException("Migration through a linked file is not supported.");

            return full;
        }

        // Only the file types the analysis converts can be written by an export
        static void CheckMigratedPath(string path)
        {
            SafePath(path);
            if (!MigratedExtensions.Contains(Path.GetExtension(path)))
                throw new InvalidDataException("Unsupported migration file type: " + path);
        }

        static bool IsLink(string path) => (File.GetAttributes(path) & FileAttributes.ReparsePoint) != 0;
        #endregion

        public static string Hash(byte[] bytes)
        {
            using var sha = SHA256.Create();
            return BitConverter.ToString(sha.ComputeHash(bytes)).Replace("-", "").ToLowerInvariant();
        }
    }
}
