using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;

namespace Evo.UI.Migration
{
    /// <summary>
    /// Tools > Evo UI > Migration Tool. Takes the migration one step at a time: analyze, review and export in v1,
    /// install v2, then apply the export and finish. The step follows from what's installed, so the window picks up
    /// where the migration is after packages are swapped or the editor restarts.
    /// This part holds the frame: header with the steps, message banner and footer. The pages are in MigrationWindow.Pages.cs,
    /// and the asset list for reviewing and applying in MigrationWindow.Review.cs.
    /// </summary>
#if UNITY_6000_5_OR_NEWER
    [Unity.Scripting.LifecycleManagement.NoAutoStaticsCleanup]
#endif
    internal sealed partial class MigrationWindow : EditorWindow
    {
        enum Step { Analyze, Review, Export, Install, Apply, Finish }

        const string DocumentationUrl = "https://evo.michsky.com/docs/evo-ui/migration";
        static readonly string[] StepNames = { "Analyze", "Review", "Export", "Install v2", "Apply", "Finish" };

        [SerializeField] Step step;
        [SerializeField] AnalysisOptions options = new();
        [SerializeField] bool automaticExport = true;
        [SerializeField] bool keepRemovedV1;
        [SerializeField] string manifestPath;
        [SerializeField] string message;
        [SerializeField] CalloutType messageType;

        // Read from the project on every refresh
        InstalledVersion version;
        AutomaticState automatic;
        List<RelatedAsset> related = new();
        MigrationManifest manifest;

        AnalysisRun analysis;
        int analysisId;
        bool analyzing;
        bool cancelRequested;

        VisualElement steps;
        Label versionChip;
        ToolbarMenu moreMenu;
        VisualElement banner;
        VisualElement page;
        VisualElement status;
        Label statusLabel;
        MigrationUI.Spinner spinner;
        MigrationUI.ProgressTrack progress;
        VisualElement footerButtons;

        #region Window
        [MenuItem("Tools/Evo UI/Migration Tool", false, 100)]
        static void Open() => GetOrCreate();

        static MigrationWindow GetOrCreate()
        {
            bool isNew = !HasOpenInstances<MigrationWindow>();
            var window = GetWindow<MigrationWindow>();
            if (isNew)
                window.position = new Rect(window.position.x, window.position.y, 920, 640);

            return window;
        }

        /// <summary>
        /// Opens the window on the outcome of an automatic migration, with its export loaded.
        /// </summary>
        internal static void ShowAutomaticResult(string exportPath, string text, bool failed)
        {
            MigrationWindow window = GetOrCreate();
            window.manifest = null;
            window.manifestPath = exportPath;
            if (exportPath != null)
                window.Guard(() => window.LoadManifest(exportPath));

            window.step = failed ? Step.Apply : Step.Finish;
            window.ShowMessage(text, failed ? CalloutType.Error : CalloutType.Success);
            window.Refresh();
            window.ShowNotification(new GUIContent(failed ? "Automatic migration stopped" : "Migration complete"), 2);
        }

        void OnEnable()
        {
            titleContent = new GUIContent("Evo UI Migration");
            minSize = new Vector2(680, 460);

            if (!string.IsNullOrEmpty(manifestPath) && File.Exists(manifestPath))
            {
                try { LoadManifest(manifestPath); }
                catch (Exception exception) { ShowMessage(exception.Message, CalloutType.Error); }
            }

            EditorApplication.update += Tick;
            EditorApplication.projectChanged += OnProjectChanged;
        }

        void OnDisable()
        {
            EditorApplication.update -= Tick;
            EditorApplication.projectChanged -= OnProjectChanged;

            // The helper can't outlive the window that reads its result
            analysisId++;
            analyzing = false;
            analysis?.Discard();
            analysis = null;
        }

        public void CreateGUI()
        {
            VisualElement root = rootVisualElement;
            root.Clear();
            root.AddToClassList("migration-window");
            MigrationUI.Apply(root, LoadStyleSheet());

            root.Add(CreateHeader());

            banner = new VisualElement();
            banner.AddToClassList("migration-banner");
            root.Add(banner);

            page = new VisualElement();
            page.AddToClassList("migration-page");
            root.Add(page);

            root.Add(CreateFooter());
            Refresh();
        }

        // Next to this script, so a sheet with the same name elsewhere is never picked up
        StyleSheet LoadStyleSheet()
        {
            string script = AssetDatabase.GetAssetPath(MonoScript.FromScriptableObject(this));
            string path = Path.GetDirectoryName(script)?.Replace('\\', '/') + "/MigrationWindow.uss";
            var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>(path);
            if (styleSheet == null)
                Debug.LogWarning("Evo UI migration: couldn't find MigrationWindow.uss next to the window's script. Reimport the tool to restore it.");

            return styleSheet;
        }

        void Tick()
        {
            spinner?.Step();
            if (analysis == null)
                return;

            if (analysis.UpdateProgress() && !cancelRequested)
                ShowAnalysisProgress();

            if (analysis.HasExited)
                FinishAnalysis();
        }

        // Packages arriving or leaving change what the window can do
        void OnProjectChanged()
        {
            if (page == null || analyzing)
                return;

            if (MigrationEngine.DetectVersion() != version || MigrationAutomation.Pending != (automatic != null))
                Refresh();
        }
        #endregion

        #region State
        /// <summary>
        /// Reads what's installed again and rebuilds the header and the current page.
        /// </summary>
        void Refresh()
        {
            if (page == null)
                return;

            version = MigrationEngine.DetectVersion();
            related = MigrationEngine.FindRelatedAssets();
            try { automatic = MigrationAutomation.State; }
            catch (Exception exception)
            {
                automatic = null;
                ShowMessage(exception.Message, CalloutType.Error);
            }

            step = Resolve(step);
            UpdateHeader();
            UpdateBanner();
            BuildPage();
        }

        // The step to show for the one asked for: itself if it fits what's installed, otherwise where the migration is
        Step Resolve(Step requested)
        {
            if (automatic != null)
                return version == InstalledVersion.V2 ? Step.Apply : Step.Install;

            return version switch
            {
                InstalledVersion.V1 when requested == Step.Analyze || (manifest != null && requested is Step.Review or Step.Export) => requested,
                InstalledVersion.V1 => Step.Analyze,
                InstalledVersion.V2 => requested == Step.Finish ? Step.Finish : Step.Apply,
                _ => Step.Install
            };
        }

        void GoTo(Step target)
        {
            step = target;
            Refresh();
        }

        void LoadManifest(string path)
        {
            MigrationManifest loaded = MigrationEngine.Load(path);
            if (MigrationEngine.DetectVersion() == InstalledVersion.V2)
            {
                MigrationEngine.Preview(loaded);
                MigrationEngine.SelectReady(loaded);
            }

            manifest = loaded;
            manifestPath = path;
        }
        #endregion

        #region Header
        VisualElement CreateHeader()
        {
            var header = new VisualElement();
            header.AddToClassList("migration-header");

            var row = new VisualElement();
            row.AddToClassList("migration-header__row");
            header.Add(row);

            row.Add(MigrationUI.CreateTitle("Evo UI Migration"));

            var spacer = new VisualElement { pickingMode = PickingMode.Ignore };
            spacer.AddToClassList("migration-header__spacer");
            row.Add(spacer);

            versionChip = MigrationUI.CreateChip("");
            row.Add(versionChip);

            moreMenu = MigrationUI.CreateMoreMenu("More");
            moreMenu.menu.AppendAction("Open Documentation", _ => OpenDocumentation());
            moreMenu.menu.AppendAction("Show Migration Data", _ => EditorUtility.RevealInFinder(MigrationEngine.StateDirectory),
                _ => Directory.Exists(MigrationEngine.StateDirectory) ? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Disabled);
            row.Add(moreMenu);

            steps = new VisualElement();
            steps.AddToClassList("migration-steps");
            header.Add(steps);

            return header;
        }

        void UpdateHeader()
        {
            (string text, string tint) chip = (automatic != null, version) switch
            {
                (true, InstalledVersion.None) => ("Waiting for Evo UI v2", "evo-chip--accent"),
                (true, InstalledVersion.V2) => ("Applying the export", "evo-chip--accent"),
                (_, InstalledVersion.V1) => ("Evo UI v1 installed", null),
                (_, InstalledVersion.V2) => ("Evo UI v2 installed", null),
                (_, InstalledVersion.Mixed) => ("v1 and v2 installed", "evo-chip--warning"),
                _ => ("Evo UI not installed", "evo-chip--warning")
            };
            versionChip.text = chip.text;
            SetTint(versionChip, chip.tint);

            steps.Clear();
            for (int i = 0; i < StepNames.Length; i++)
            {
                if (i > 0)
                {
                    var line = new VisualElement { pickingMode = PickingMode.Ignore };
                    line.AddToClassList("migration-step__line");
                    line.EnableInClassList("migration-step__line--done", i <= (int)step);
                    steps.Add(line);
                }

                steps.Add(CreateStep((Step)i));
            }
        }

        // Earlier steps show a check mark. Steps that fit what's installed can be clicked, such as going back from Export to Review.
        VisualElement CreateStep(Step item)
        {
            bool done = item < step;
            bool link = item != step && Resolve(item) == item;

            var element = new VisualElement();
            element.AddToClassList("migration-step");
            element.EnableInClassList("migration-step--current", item == step);
            element.EnableInClassList("migration-step--done", done);
            element.EnableInClassList("migration-step--link", link);
            if (link)
            {
                element.tooltip = "Go to " + StepNames[(int)item];
                element.RegisterCallback<ClickEvent>(_ => GoTo(item));
            }

            var number = new Label(done ? "" : ((int)item + 1).ToString()) { pickingMode = PickingMode.Ignore };
            number.AddToClassList("migration-step__number");
            element.Add(number);

            if (done)
            {
                var check = new VisualElement { pickingMode = PickingMode.Ignore };
                check.style.backgroundImage = MigrationUI.GetIcon("d_checkmark");
                check.AddToClassList("migration-step__check");
                number.Add(check);
            }

            var name = new Label(StepNames[(int)item]) { pickingMode = PickingMode.Ignore };
            name.AddToClassList("migration-step__name");
            element.Add(name);

            return element;
        }

        static void SetTint(VisualElement chip, string tint)
        {
            foreach (string className in new[] { "evo-chip--accent", "evo-chip--success", "evo-chip--warning", "evo-chip--error" })
                chip.EnableInClassList(className, className == tint);
        }

        static void OpenDocumentation() => Application.OpenURL(DocumentationUrl);
        #endregion

        #region Banner
        /// <summary>
        /// Shows the outcome of the last action between the header and the page, until it's dismissed or replaced.
        /// </summary>
        void ShowMessage(string text, CalloutType type = CalloutType.Info)
        {
            message = text;
            messageType = type;
            UpdateBanner();
        }

        void ClearMessage() => ShowMessage(null);

        void UpdateBanner()
        {
            if (banner == null)
                return;

            banner.Clear();
            banner.style.display = string.IsNullOrEmpty(message) ? DisplayStyle.None : DisplayStyle.Flex;
            if (string.IsNullOrEmpty(message))
                return;

            VisualElement callout = MigrationUI.CreateCallout(message, messageType);
            callout.Add(MigrationUI.CreateIconButton(MigrationUI.GetIcon("d_clear"), "Dismiss", ClearMessage));
            banner.Add(callout);
        }

        /// <summary>
        /// Runs an action, showing what went wrong in the banner instead of only in the Console.
        /// </summary>
        void Guard(Action action)
        {
            try { action(); }
            catch (Exception exception)
            {
                ShowMessage(exception.Message, CalloutType.Error);
                Debug.LogException(exception);
            }
        }
        #endregion

        #region Footer
        VisualElement CreateFooter()
        {
            var footer = new VisualElement();
            footer.AddToClassList("migration-footer");

            status = new VisualElement();
            status.AddToClassList("migration-status");
            footer.Add(status);

            spinner = new MigrationUI.Spinner();
            status.Add(spinner);

            var text = new VisualElement();
            text.AddToClassList("migration-status__text");
            status.Add(text);

            statusLabel = new Label();
            statusLabel.AddToClassList("migration-status__label");
            text.Add(statusLabel);

            progress = new MigrationUI.ProgressTrack();
            text.Add(progress);

            footerButtons = new VisualElement();
            footerButtons.AddToClassList("migration-footer__buttons");
            footer.Add(footerButtons);

            return footer;
        }

        /// <summary>
        /// The footer's text, with a spinner while waiting and a track while progress is known.
        /// </summary>
        void SetStatus(string text, bool warning = false, bool spinning = false, float? fraction = null)
        {
            statusLabel.text = text ?? "";
            status.EnableInClassList("migration-status--warning", warning);
            spinner.style.display = spinning ? DisplayStyle.Flex : DisplayStyle.None;
            progress.style.display = fraction.HasValue ? DisplayStyle.Flex : DisplayStyle.None;
            progress.SetProgress(fraction ?? 0);
        }

        /// <summary>
        /// The page's actions, right-aligned, with the main one last. Null entries are skipped.
        /// </summary>
        void SetButtons(params Button[] buttons)
        {
            footerButtons.Clear();
            foreach (Button button in buttons)
            {
                if (button != null)
                    footerButtons.Add(button);
            }
        }
        #endregion

        #region Analysis
        void StartAnalysis()
        {
            if (analyzing)
                return;

            int id = ++analysisId;
            analyzing = true;
            cancelRequested = false;
            ClearMessage();
            SetBusy(true);

            // Let the disabled page draw first: collecting Unity's compilation data holds the editor for a moment
            EditorApplication.delayCall += () =>
            {
                if (this == null || id != analysisId)
                    return;

                try { analysis = AnalysisRun.Start(options); }
                catch (Exception exception)
                {
                    analyzing = false;
                    SetBusy(false);
                    ShowMessage(exception.Message, CalloutType.Error);
                    Debug.LogException(exception);
                }
            };
        }

        void CancelAnalysis()
        {
            if (!analyzing || cancelRequested)
                return;

            cancelRequested = true;
            if (analysis != null)
            {
                analysis.Cancel();
                ShowAnalysisProgress();
                return;
            }

            // Not started yet
            analysisId++;
            analyzing = false;
            SetBusy(false);
            ShowMessage("Analysis canceled. The previous results are unchanged.");
        }

        void FinishAnalysis()
        {
            AnalysisRun run = analysis;
            analysis = null;
            analyzing = false;
            bool succeeded = false;
            try
            {
                if (cancelRequested)
                {
                    ShowMessage("Analysis canceled. The previous results are unchanged.");
                    return;
                }

                manifest = run.LoadResult();
                manifestPath = run.OutputPath;
                succeeded = true;
                shownGuid = null;
                step = Step.Review;

                int notes = manifest.notes.Count;
                if (notes > 0)
                    ShowMessage($"{MigrationUI.Plural(notes, "analysis note")} about files that were skipped or only partly converted. See them in the overview.", CalloutType.Warning);
            }
            catch (Exception exception)
            {
                ShowMessage(exception.Message, CalloutType.Error);
                Debug.LogException(exception);
            }
            finally
            {
                // The new export replaces earlier runs. A canceled or failed run leaves nothing behind.
                if (succeeded)
                {
                    run.Dispose();
                    AnalysisRun.DeleteOtherRuns(run.OutputPath);
                }
                else
                    run.Discard();

                SetBusy(false);
            }
        }

        // While analyzing, only Cancel works
        void SetBusy(bool busy)
        {
            page.SetEnabled(!busy);
            steps.SetEnabled(!busy);
            moreMenu.SetEnabled(!busy);
            if (!busy)
            {
                Refresh();
                return;
            }

            SetButtons(MigrationUI.CreateButton("Cancel", CancelAnalysis, "Stop analyzing. Nothing in the project changes."));
            ShowAnalysisProgress();
        }

        void ShowAnalysisProgress()
        {
            if (cancelRequested)
            {
                SetStatus("Canceling analysis…", spinning: true);
                return;
            }

            int percent = analysis?.Percent ?? 0;
            SetStatus($"{percent}%  ·  {analysis?.Status ?? "Preparing analysis…"}", spinning: true, fraction: percent / 100f);
        }
        #endregion
    }
}
