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

namespace Evo.UI.Migration
{
    // One page per step, except Review and Apply's asset list (MigrationWindow.Review.cs)
    internal sealed partial class MigrationWindow
    {
        static readonly (int codePage, string name)[] Encodings =
        {
            (0, "None, skip those scripts"),
            (1252, "Western European (1252)"),
            (932, "Japanese (932)"),
            (936, "Simplified Chinese (936)"),
            (949, "Korean (949)"),
            (950, "Traditional Chinese (950)"),
            (1251, "Cyrillic (1251)")
        };

        // Controls a page updates in place
        Label folderHint;
        Button analyzeButton;
        VisualElement automaticOption;
        VisualElement manualOption;
        Toggle keepRemovedToggle;
        Button exportButton;

        void BuildPage()
        {
            page.Clear();
            SetStatus(null);
            SetButtons();

            // The review list belongs to its page, so updates to it stop when another page shows
            changeList = null;

            switch (step)
            {
                case Step.Analyze: BuildAnalyzePage(); break;
                case Step.Review: BuildReviewPage(); break;
                case Step.Export: BuildExportPage(); break;
                case Step.Install: BuildInstallPage(); break;
                case Step.Apply when automatic != null: BuildApplyingPage(); break;
                case Step.Apply when manifest == null: BuildLoadPage(); break;
                case Step.Apply: BuildReviewPage(); break;
                case Step.Finish: BuildFinishPage(); break;
            }
        }

        #region Analyze
        void BuildAnalyzePage()
        {
            VisualElement column = CreateColumn();
            column.Add(MigrationUI.CreateTitle("Analyze your project"));
            column.Add(CreateLead("Finds where your scenes, prefabs, scripts and other assets use Evo UI v1, and plans their changes for v2. "
                + "Analyzing only reads your project."));

            column.Add(MigrationUI.CreateHeading("Folder"));
            var folderRow = new VisualElement();
            folderRow.AddToClassList("migration-folder");
            column.Add(folderRow);

            TextField folderField = MigrationUI.CreateTextField(options.folder, "The folder to analyze, with its subfolders", value =>
            {
                options.folder = value;
                UpdateAnalyzeState();
            });
            folderRow.Add(folderField);
            folderRow.Add(MigrationUI.CreateButton("Browse…", () => BrowseFolder(folderField), "Choose a folder under Assets"));

            folderHint = MigrationUI.CreateDescription();
            column.Add(folderHint);

            column.Add(MigrationUI.CreateHeading("Asset types"));
            var types = new VisualElement();
            types.AddToClassList("migration-types");
            column.Add(types);

            types.Add(MigrationUI.CreateToggle("Prefabs", options.prefabs, "Prefab assets (.prefab)", value => SetType(ref options.prefabs, value)));
            types.Add(MigrationUI.CreateToggle("Scenes", options.scenes, "Scenes (.unity)", value => SetType(ref options.scenes, value)));
            types.Add(MigrationUI.CreateToggle("Scripts", options.scripts, "C# scripts that use the Evo UI API (.cs)", value => SetType(ref options.scripts, value)));
            types.Add(MigrationUI.CreateToggle("Other assets", options.otherAssets,
                "ScriptableObject assets, animation clips and animator controllers saved as text", value => SetType(ref options.otherAssets, value)));

            column.Add(MigrationUI.CreateHeading("Script encoding"));
            int encoding = Math.Max(0, Array.FindIndex(Encodings, entry => entry.codePage == options.legacyCodePage));
            column.Add(MigrationUI.CreateDropdown("Legacy encoding", Encodings.Select(entry => entry.name).ToList(), encoding,
                "For scripts without a BOM that aren't valid UTF-8. Choose their actual encoding; it can't be reliably guessed.",
                index => options.legacyCodePage = Encodings[index].codePage));
            column.Add(MigrationUI.CreateDescription("UTF-8, and UTF-16 or UTF-32 with a BOM, are detected automatically. If analysis notes list scripts "
                + "that aren't valid UTF-8, choose their encoding here and analyze again, or resave them as UTF-8."));

            column.Add(MigrationUI.CreateHeading("What gets converted"));
            column.Add(CreateCoverage());
            column.Add(MigrationUI.CreateLink("See the full list in the documentation", DocumentationUrl));

            Button reviewButton = manifest != null ? MigrationUI.CreateButton("Review Results", () => GoTo(Step.Review)) : null;
            analyzeButton = MigrationUI.CreatePrimaryButton(manifest != null ? "Analyze Again" : "Analyze Project", () => Guard(StartAnalysis));
            SetButtons(reviewButton, analyzeButton);
            UpdateAnalyzeState();
        }

        void SetType(ref bool option, bool value)
        {
            option = value;
            UpdateAnalyzeState();
        }

        void UpdateAnalyzeState()
        {
            string folderIssue = GetFolderIssue(options.folder);
            folderHint.text = folderIssue ?? "Evo's package folders and this tool are always skipped.";
            folderHint.EnableInClassList("evo-description--warning", folderIssue != null);

            string issue = folderIssue ?? (options.HasAssetTypes ? null : "Choose at least one asset type to analyze.");
            analyzeButton.SetEnabled(issue == null);
            if (issue != null)
                SetStatus(issue, true);
            else if (manifest != null)
                SetStatus($"Last analysis: {MigrationUI.Plural(manifest.changes.Count, "asset")}, {manifest.changes.Count(change => change.NeedsReview)} with review notes.");
            else
                SetStatus("Save your scenes and assets before analyzing.");
        }

        static string GetFolderIssue(string folder)
        {
            folder = (folder ?? "").Replace('\\', '/').TrimEnd('/');
            if (folder != "Assets" && !folder.StartsWith("Assets/", StringComparison.Ordinal))
                return "Choose a folder under Assets.";

            return Directory.Exists(Path.Combine(MigrationEngine.Root, folder)) ? null : "This folder doesn't exist.";
        }

        void BrowseFolder(TextField field)
        {
            string current = Path.Combine(MigrationEngine.Root, options.folder ?? "");
            string chosen = EditorUtility.OpenFolderPanel("Choose a folder to analyze", Directory.Exists(current) ? current : Application.dataPath, "");
            if (string.IsNullOrEmpty(chosen))
                return;

            string root = MigrationEngine.Root.Replace('\\', '/').TrimEnd('/') + "/";
            string full = Path.GetFullPath(chosen).Replace('\\', '/');
            string relative = full.StartsWith(root, StringComparison.OrdinalIgnoreCase) ? full.Substring(root.Length) : null;
            if (relative != "Assets" && relative?.StartsWith("Assets/", StringComparison.Ordinal) != true)
            {
                ShowMessage("Choose a folder inside this project's Assets folder.", CalloutType.Warning);
                return;
            }

            field.value = relative;
        }

        static VisualElement CreateCoverage()
        {
            var coverage = new VisualElement();
            coverage.AddToClassList("evo-well");
            coverage.AddToClassList("migration-coverage");

            coverage.Add(CreateCoverageColumn("Converted for you",
                "Component scripts and C# type names, fields that became properties, text provider casts, styling sources, context menu submenus, "
                + "Popover listeners, and the built-in animations of Modal Window, Notification, Popover, Tabs, Dropdown, Tooltip and Context Menu. "
                + "Asset GUIDs and component file IDs stay the same, so references keep working.", null));

            coverage.Add(CreateCoverageColumn("Left for you to review",
                "UI Animator, Countdown, navigation replacements, redesigned effects and graphics, custom animation code, inactive #if branches, "
                + "complex prefab overrides and unusual Popover listener patterns. Each affected asset lists what to check.",
                "migration-coverage__column--second"));

            return coverage;
        }

        static VisualElement CreateCoverageColumn(string title, string text, string className)
        {
            var column = new VisualElement();
            column.AddToClassList("migration-coverage__column");
            if (className != null)
                column.AddToClassList(className);

            var titleLabel = new Label(title);
            titleLabel.AddToClassList("migration-coverage__title");
            column.Add(titleLabel);
            column.Add(MigrationUI.CreateDescription(text));
            return column;
        }
        #endregion

        #region Export
        void BuildExportPage()
        {
            string unavailable = GetAutomaticIssue();
            int migrated = manifest.changes.Count(change => change.selected && change.HasChanges);
            int reviewOnly = manifest.changes.Count(change => !change.HasChanges);

            VisualElement column = CreateColumn();
            column.Add(MigrationUI.CreateTitle("Export the migration plan"));
            column.Add(CreateLead($"{MigrationUI.Plural(migrated, "asset")} will be migrated"
                + (reviewOnly > 0 ? $", and {MigrationUI.Plural(reviewOnly, "asset")} only {(reviewOnly == 1 ? "has" : "have")} review notes" : "")
                + ". The export keeps both, with the analysis notes. Choose how to continue:"));

            automaticOption = CreateOption("Automatic", unavailable == null ? "Recommended" : null,
                unavailable == null
                    ? "Removes Evo UI v1 right after saving the export, and keeps your own files in its folders. Import Evo UI v2 next: "
                    + "the export is applied as soon as the import finishes. Until then, you can restore v1 from this window."
                    : "Not available: " + unavailable,
                () => SetExportMode(true));
            automaticOption.SetEnabled(unavailable == null);
            column.Add(automaticOption);

            keepRemovedToggle = MigrationUI.CreateToggle("Keep removed v1 files", keepRemovedV1,
                "Removed v1 files are kept in Library/EvoUIMigration until v2 is applied, so v1 can be restored. Turn this on to keep them afterwards too.",
                value => keepRemovedV1 = value);
            automaticOption.Q(className: "migration-option__text").Add(keepRemovedToggle);

            manualOption = CreateOption("Manual", null,
                "Only saves the export. Remove Evo UI v1 and import v2 yourself, keeping this tool folder, then load the export here and apply it. "
                + "Don't resave the listed assets in between.",
                () => SetExportMode(false));
            column.Add(manualOption);

            if (related.Count > 0)
                column.Add(CreateRelatedCallout("after exporting"));

            column.Add(MigrationUI.CreateCallout("Keep a backup of your project. Applying backs up every file it replaces, but only inside this project's Library folder."));

            exportButton = MigrationUI.CreatePrimaryButton("Export…", () => Guard(Export));
            SetButtons(MigrationUI.CreateButton("Back", () => GoTo(Step.Review)), exportButton);
            SetExportMode(automaticExport && unavailable == null, false);
        }

        // Automatic mode needs a v1 installation it can verify, file by file, against the known v1 releases
        static string GetAutomaticIssue()
        {
            try
            {
                MigrationAutomation.Plan();
                return null;
            }
            catch (Exception exception) { return exception.Message; }
        }

        VisualElement CreateOption(string title, string badge, string description, Action onSelect)
        {
            var option = new VisualElement();
            option.AddToClassList("migration-option");
            option.RegisterCallback<ClickEvent>(_ => onSelect());

            var radio = new VisualElement { pickingMode = PickingMode.Ignore };
            radio.AddToClassList("migration-option__radio");
            option.Add(radio);

            var dot = new VisualElement { pickingMode = PickingMode.Ignore };
            dot.AddToClassList("migration-option__dot");
            radio.Add(dot);

            var text = new VisualElement();
            text.AddToClassList("migration-option__text");
            option.Add(text);

            var titleRow = new VisualElement { pickingMode = PickingMode.Ignore };
            titleRow.AddToClassList("migration-option__title-row");
            text.Add(titleRow);

            var titleLabel = new Label(title) { pickingMode = PickingMode.Ignore };
            titleLabel.AddToClassList("migration-option__title");
            titleRow.Add(titleLabel);
            if (badge != null)
                titleRow.Add(MigrationUI.CreateChip(badge, "evo-chip--accent"));

            Label descriptionLabel = MigrationUI.CreateDescription(description);
            descriptionLabel.pickingMode = PickingMode.Ignore;
            text.Add(descriptionLabel);

            return option;
        }

        void SetExportMode(bool automaticMode, bool remember = true)
        {
            if (remember)
                automaticExport = automaticMode;

            automaticOption.EnableInClassList("migration-option--selected", automaticMode);
            manualOption.EnableInClassList("migration-option--selected", !automaticMode);
            keepRemovedToggle.style.display = automaticMode ? DisplayStyle.Flex : DisplayStyle.None;

            exportButton.text = automaticMode ? "Export and Remove v1…" : "Export…";
            exportButton.userData = automaticMode;
            SetStatus(automaticMode ? "Evo UI v1 is removed after the export is saved." : "Exporting doesn't change your project.");
        }

        void Export()
        {
            bool automaticMode = exportButton.userData is true;

            // Review-only entries go too, even without changes to write
            var export = new MigrationManifest
            {
                format = manifest.format,
                sourceVersion = manifest.sourceVersion,
                targetVersion = manifest.targetVersion,
                createdUtc = manifest.createdUtc,
                project = manifest.project,
                notes = manifest.notes,
                kept = manifest.kept,
                changes = manifest.changes.Where(change => change.selected || !change.HasChanges).ToList()
            };

            var exported = new HashSet<string>(export.changes.Select(change => change.guid));
            foreach (MigrationChange change in export.changes)
            {
                string missing = change.requires.FirstOrDefault(guid => !exported.Contains(guid));
                if (missing != null)
                    throw new InvalidOperationException("Select the required dependency before exporting: " + (manifest.Find(missing)?.path ?? missing));
            }

            if (automaticMode)
            {
                if (!EditorUtility.DisplayDialog("Export and remove Evo UI v1",
                    "The export is saved first. Then Evo UI v1 is removed, keeping your own files in its folders, and open scenes are closed until the migration is done.\n\n"
                    + "Import Evo UI v2 next. Until it's installed, you can restore v1 from this window.", "Export and Remove", "Cancel"))
                    return;

                MigrationEngine.CheckSaved();
            }

            string output = EditorUtility.SaveFilePanel("Export migration data", MigrationEngine.Root, "Evo UI Migration", "json");
            if (string.IsNullOrEmpty(output))
                return;

            File.WriteAllText(output, JsonUtility.ToJson(export, true));
            MigrationEngine.RememberExport(output);
            if (!automaticMode)
            {
                ShowMessage($"Exported {MigrationUI.Plural(export.changes.Count, "asset")} to {output}. Next, remove Evo UI v1 and import v2, "
                    + "keeping this tool folder, then open this window again to apply the export.", CalloutType.Success);
                return;
            }

            MigrationAutomation.Start(output, keepRemovedV1);
            LoadManifest(MigrationAutomation.Full(MigrationAutomation.State.export));
            ShowMessage($"Exported {MigrationUI.Plural(export.changes.Count, "asset")} and removed Evo UI v1.", CalloutType.Success);
            Refresh();
        }
        #endregion

        #region Install
        void BuildInstallPage()
        {
            if (automatic != null)
            {
                BuildWaitingPage();
                return;
            }

            VisualElement column = CreateColumn();
            if (version == InstalledVersion.Mixed)
            {
                column.Add(MigrationUI.CreateTitle("Remove the remaining v1 files"));
                column.Add(CreateLead("Both Evo UI v1 and v2 files are installed, and a mixed installation can't be migrated. "
                    + "Remove what's left of Evo UI v1, then import Evo UI v2 again if anything is missing."));

                // The v1 script the version check found, which shows where the rest of v1 is
                column.Add(MigrationUI.CreateHeading("Evo UI v1 script found"));
                foreach (string marker in MigrationEngine.FindMarkerScripts("ProceduralRect"))
                    column.Add(CreateAssetRow(marker));
            }
            else
            {
                column.Add(MigrationUI.CreateTitle("Install Evo UI v2"));
                column.Add(CreateLead("Evo UI isn't installed in this project. If you're migrating manually, import Evo UI v2 now, "
                    + "keeping this tool folder and your own assets. Then load your export here."));

                string lastExport = MigrationEngine.LastExport;
                if (lastExport != null)
                {
                    column.Add(MigrationUI.CreateHeading("Your export"));
                    column.Add(CreatePathRow(lastExport));
                }

                column.Add(MigrationUI.CreateDescription("To analyze a project instead, install Evo UI v1."));
                SetButtons(MigrationUI.CreatePrimaryButton("Open Package Manager", OpenPackageManager));
            }

            if (related.Count > 0)
                column.Add(CreateRelatedCallout(null));
        }

        // Automatic mode removed v1 and waits for v2
        void BuildWaitingPage()
        {
            VisualElement column = CreateColumn();
            column.Add(MigrationUI.CreateTitle("Import Evo UI v2"));
            column.Add(CreateLead("Evo UI v1 was removed and your export is saved. Import Evo UI v2 from Package Manager > My Assets, "
                + "or from its .unitypackage. The export is applied as soon as the import finishes, and the results show here."));

            if (version == InstalledVersion.V1)
                column.Add(MigrationUI.CreateCallout("Evo UI v1 is installed again, so the automatic migration can't continue. Cancel it to analyze your project again.", CalloutType.Warning));
            else if (version == InstalledVersion.Mixed)
                column.Add(MigrationUI.CreateCallout("Both Evo UI v1 and v2 files are installed. Remove the remaining v1 files to continue.", CalloutType.Warning));
            else
                column.Add(MigrationUI.CreateCallout("Until v2 is imported, Console errors about missing Evo UI types are expected."));

            if (related.Count > 0)
                column.Add(CreateRelatedCallout(null));

            column.Add(MigrationUI.CreateHeading("Options"));
            column.Add(MigrationUI.CreateToggle("Keep removed v1 files", automatic.keepRemoved,
                "Removed v1 files are kept in Library/EvoUIMigration until v2 is applied, so v1 can be restored. Turn this on to keep them afterwards too.",
                value => Guard(() =>
                {
                    MigrationAutomation.SetKeepRemoved(value);
                    automatic.keepRemoved = value;
                    keepRemovedV1 = value;
                })));

            column.Add(MigrationUI.CreateHeading("Files"));
            column.Add(CreatePathRow(automatic.source, "Export"));
            column.Add(CreatePathRow(MigrationAutomation.Full(automatic.removed), "Removed v1 files"));

            if (automatic.kept.Count > 0)
            {
                column.Add(MigrationUI.CreateHeading($"Kept in the Evo folders ({automatic.kept.Count})"));
                column.Add(MigrationUI.CreateDescription("These aren't part of Evo UI v1, so they stay where they are."));
                column.Add(CreateListBox(automatic.kept, 50));
            }

            switch (version)
            {
                case InstalledVersion.None:
                    SetStatus("Waiting for Evo UI v2…", spinning: true);
                    SetButtons(
                        MigrationUI.CreateButton("Restore Evo UI v1", () => Guard(RestoreV1), "Put v1 back and cancel the automatic migration"),
                        MigrationUI.CreateButton("Continue Manually", () => Guard(CancelAutomatic), "Keep v1 removed and apply the export yourself"),
                        MigrationUI.CreatePrimaryButton("Open Package Manager", OpenPackageManager));
                    break;

                case InstalledVersion.V1:
                    SetButtons(MigrationUI.CreatePrimaryButton("Cancel Automatic Migration", () => Guard(CancelAutomatic)));
                    break;

                default:
                    SetButtons(MigrationUI.CreateButton("Continue Manually", () => Guard(CancelAutomatic), "Keep v1 removed and apply the export yourself"));
                    break;
            }
        }

        // v2 arrived, and the automatic migration applies the export once Unity is idle
        void BuildApplyingPage()
        {
            VisualElement column = CreateColumn();
            column.Add(MigrationUI.CreateTitle("Applying your export"));
            column.Add(CreateLead("Evo UI v2 is installed. Your export is applied as soon as Unity finishes importing and compiling, and the results show here."));
            SetStatus("Waiting for Unity to finish importing…", spinning: true);

            if (automatic.stage == MigrationAutomation.AwaitingV2)
                SetButtons(MigrationUI.CreateButton("Apply Manually", () => Guard(CancelAutomatic), "Stop the automatic migration and review the export yourself"));
        }

        void RestoreV1()
        {
            MigrationAutomation.RestoreV1();
            ShowMessage("Evo UI v1 was restored, and the automatic migration canceled.", CalloutType.Success);
            step = manifest != null ? Step.Review : Step.Analyze;
            Refresh();
        }

        void CancelAutomatic()
        {
            string source = automatic?.source;
            string export = MigrationAutomation.Abandon();
            if (export != null && File.Exists(export))
                LoadManifest(export);

            ShowMessage(version == InstalledVersion.V1
                ? "Automatic migration canceled."
                : $"Automatic migration canceled. Install Evo UI v2, then apply the loaded export here. It's also saved at {source}.");
            Refresh();
        }

        static void OpenPackageManager() => UnityEditor.PackageManager.UI.Window.Open("");
        #endregion

        #region Apply
        void BuildLoadPage()
        {
            var empty = new VisualElement();
            empty.AddToClassList("migration-empty");
            page.Add(empty);

            var title = new Label("Load your export");
            title.AddToClassList("migration-empty__title");
            empty.Add(title);

            var text = new Label("Choose the JSON file you exported from your Evo UI v1 project. Loading only checks the files it lists, "
                + "and nothing changes until you apply.");
            text.AddToClassList("migration-empty__message");
            empty.Add(text);

            string lastExport = MigrationEngine.LastExport;
            if (lastExport != null)
                empty.Add(MigrationUI.CreateButton($"Load {Path.GetFileName(lastExport)}", () => Guard(() => OpenExport(lastExport)), lastExport));

            SetStatus("No export loaded.");
            SetButtons(MigrationUI.CreatePrimaryButton("Load Export…", () => Guard(ChooseExport)));
        }

        void ChooseExport()
        {
            string folder = Path.GetDirectoryName(MigrationEngine.LastExport ?? MigrationEngine.Root + "/");
            string path = EditorUtility.OpenFilePanel("Load migration export", folder, "json");
            if (!string.IsNullOrEmpty(path))
                OpenExport(path);
        }

        void OpenExport(string path)
        {
            ClearMessage();
            LoadManifest(path);
            shownGuid = null;
            step = Step.Apply;
            Refresh();
        }

        void ApplySelected()
        {
            int count = manifest.changes.Count(change => change.selected && change.HasChanges && change.status == ChangeStatus.Ready);
            int kept = MigrationEngine.MissingKeptAssets(manifest).Count;
            MigrationEngine.Apply(manifest);
            MigrationEngine.Preview(manifest);
            MigrationEngine.SelectReady(manifest);

            ShowMessage($"Applied {MigrationUI.Plural(count, "asset")}"
                + (kept > 0 ? $" and copied {(kept == 1 ? "1 v1 asset" : $"{kept:N0} v1 assets")} to {MigrationEngine.KeptFolder}" : "")
                + ". The files it replaced are backed up in Library/EvoUIMigration/Backups.", CalloutType.Success);
            if (!manifest.changes.Any(change => change.HasChanges && change.status == ChangeStatus.Ready))
                step = Step.Finish;

            Refresh();
        }
        #endregion

        #region Finish
        void BuildFinishPage()
        {
            VisualElement column = CreateColumn();
            column.Add(MigrationUI.CreateTitle("Finish the migration"));
            column.Add(CreateLead("Check the migrated project, then remove this tool. Migrated assets don't depend on it."));

            int review = 0;
            int notApplied = 0;
            if (manifest != null)
            {
                int applied = manifest.changes.Count(change => change.status == ChangeStatus.Applied);
                notApplied = manifest.changes.Count(change => change.HasChanges && change.status != ChangeStatus.Applied);
                review = manifest.changes.Count(change => change.NeedsReview);

                var stats = new VisualElement();
                stats.AddToClassList("migration-stats");
                stats.Add(CreateStat(applied, "Applied", "migration-stat--success"));
                stats.Add(CreateStat(notApplied, "Not applied", notApplied > 0 ? "migration-stat--warning" : null));
                stats.Add(CreateStat(review, "Need manual review", review > 0 ? "migration-stat--warning" : null));
                column.Add(stats);
            }

            column.Add(MigrationUI.CreateHeading("Next steps"));
            var tasks = new List<(string title, string text, Button button)>
            {
                ("Check your UI in Play Mode", "Open the scenes that use Evo UI and try the migrated controls.", null)
            };

            if (review > 0)
            {
                tasks.Add(("Resolve the manual review notes", $"{MigrationUI.Plural(review, "asset")} list steps the tool couldn't do for you.",
                    MigrationUI.CreateButton("Show", ShowNeedsReview)));
            }

            if (notApplied > 0)
            {
                tasks.Add(("Apply the remaining changes", $"{MigrationUI.Plural(notApplied, "asset")} weren't applied. Their status says why.",
                    MigrationUI.CreateButton("Show", () => GoTo(Step.Apply))));
            }

            if (related.Count > 0)
            {
                tasks.Add(($"Update {MigrationEngine.JoinNames(related)} to v2", "A clean import is best: move your own files, such as settings or tables, "
                    + $"out of {(related.Count == 1 ? "its folder" : "their folders")}, delete {(related.Count == 1 ? "it" : "them")}, then import v2.", null));
            }

            tasks.Add(("Remove the migration tool", $"Migrated assets, {MigrationEngine.PresetsFolder} and {MigrationEngine.KeptFolder} stay, and nothing in them depends on the tool.", null));

            for (int i = 0; i < tasks.Count; i++)
                column.Add(CreateTask(i + 1, tasks[i].title, tasks[i].text, tasks[i].button));

            column.Add(MigrationUI.CreateHeading("Backups"));
            column.Add(MigrationUI.CreateDescription("Each apply backs up the files it replaces to Library/EvoUIMigration/Backups. Restore Last Apply puts back "
                + "the latest batch, unless those files changed since."));

            string backup = MigrationEngine.LastBackup;
            bool hasBackup = backup != null && Directory.Exists(backup);
            var buttons = new VisualElement();
            buttons.AddToClassList("migration-buttons");
            column.Add(buttons);

            Button showBackup = MigrationUI.CreateButton("Show Backup", () => EditorUtility.RevealInFinder(backup), backup);
            showBackup.SetEnabled(hasBackup);
            buttons.Add(showBackup);

            Button restore = MigrationUI.CreateButton("Restore Last Apply", () => Guard(RestoreLastApply));
            restore.SetEnabled(hasBackup);
            buttons.Add(restore);

            SetButtons(MigrationUI.CreatePrimaryButton("Remove Migration Tool…", () => Guard(RemoveTool)));
        }

        void ShowNeedsReview()
        {
            categoryFilter = AllCategories;
            reviewFilter = true;
            searchText = "";
            GoTo(Step.Apply);
        }

        void RestoreLastApply()
        {
            if (!EditorUtility.DisplayDialog("Restore last apply", "Puts back the files the last apply replaced, as they were before migrating. "
                + "If any of them changed since, nothing is restored.", "Restore", "Cancel"))
                return;

            MigrationEngine.Restore();
            if (manifest != null)
            {
                MigrationEngine.Preview(manifest);
                MigrationEngine.SelectReady(manifest);
            }

            ShowMessage("Restored the files from the last apply.", CalloutType.Success);
            Refresh();
        }

        void RemoveTool()
        {
            string root = MigrationEngine.CheckToolRemovable();
            int choice = EditorUtility.DisplayDialogComplex("Remove migration tool",
                $"Removes {root}. Migrated assets, {MigrationEngine.PresetsFolder} and {MigrationEngine.KeptFolder} stay; nothing in them depends on the tool.\n\n"
                + "Remove Tool and Data also deletes Library/EvoUIMigration: backups (used by Restore Last Apply), saved exports, kept v1 files and analysis files.",
                "Remove Tool and Data", "Cancel", "Remove Tool Only");

            if (choice == 1)
                return;

            MigrationEngine.RemoveTool(choice == 0);
            Close();
        }
        #endregion

        #region Shared
        // Scrolling page with a centered column that keeps lines readable in wide windows
        VisualElement CreateColumn()
        {
            var scroll = new ScrollView(ScrollViewMode.Vertical) { horizontalScrollerVisibility = ScrollerVisibility.Hidden };
            scroll.AddToClassList("migration-scroll");
            page.Add(scroll);

            var column = new VisualElement();
            column.AddToClassList("migration-column");
            scroll.Add(column);
            return column;
        }

        static Label CreateLead(string text)
        {
            var lead = new Label(text);
            lead.AddToClassList("migration-lead");
            return lead;
        }

        // Their folders can hold the user's own settings or tables, so other Evo products are never removed automatically
        VisualElement CreateRelatedCallout(string when)
        {
            bool one = related.Count == 1;
            string update = one ? "its v2 release, so update it" : "their v2 releases, so update them";
            return MigrationUI.CreateCallout($"{MigrationEngine.JoinNames(related, true)} {(one ? "is" : "are")} also installed. Evo UI v2 needs {update} "
                + $"{when ?? "too, if you haven't yet"}. A clean import is best: move your own files, such as settings or tables, out of "
                + $"{(one ? "its folder" : "their folders")}, delete {(one ? "it" : "them")}, then import v2.", CalloutType.Warning);
        }

        static VisualElement CreateStat(int value, string label, string className = null)
        {
            var stat = new VisualElement();
            stat.AddToClassList("migration-stat");
            if (className != null)
                stat.AddToClassList(className);

            var valueLabel = new Label(value.ToString("N0"));
            valueLabel.AddToClassList("migration-stat__value");
            stat.Add(valueLabel);

            var nameLabel = new Label(label);
            nameLabel.AddToClassList("migration-stat__label");
            stat.Add(nameLabel);
            return stat;
        }

        static VisualElement CreateTask(int number, string title, string text, Button button)
        {
            var task = new VisualElement();
            task.AddToClassList("migration-task");

            var numberLabel = new Label(number.ToString());
            numberLabel.AddToClassList("migration-task__number");
            task.Add(numberLabel);

            var textColumn = new VisualElement();
            textColumn.AddToClassList("migration-task__text");
            task.Add(textColumn);

            var titleLabel = new Label(title);
            titleLabel.AddToClassList("migration-task__title");
            textColumn.Add(titleLabel);
            textColumn.Add(MigrationUI.CreateDescription(text));

            if (button != null)
                task.Add(button);

            return task;
        }

        // A file or folder that shows in Explorer or Finder when clicked
        static VisualElement CreatePathRow(string path, string label = null)
        {
            var row = new VisualElement { tooltip = path + "\nClick to show it." };
            row.AddToClassList("migration-link-row");
            row.RegisterCallback<ClickEvent>(_ => EditorUtility.RevealInFinder(path));

            row.Add(new Image { image = MigrationUI.GetIcon(File.Exists(path) ? "TextAsset Icon" : "Folder Icon"), pickingMode = PickingMode.Ignore });
            if (label != null)
            {
                var name = new Label(label) { pickingMode = PickingMode.Ignore };
                name.AddToClassList("migration-link-row__label");
                row.Add(name);
            }

            // Long paths lose their start, so the file name stays readable
            row.Add(new Label(path) { pickingMode = PickingMode.Ignore });
            return row;
        }

        // A project folder or asset that shows in the Project window when clicked
        static VisualElement CreateAssetRow(string path)
        {
            var row = new VisualElement { tooltip = path + "\nClick to show it in the Project window." };
            row.AddToClassList("migration-link-row");
            row.RegisterCallback<ClickEvent>(_ =>
            {
                UnityEngine.Object asset = AssetDatabase.LoadMainAssetAtPath(path);
                if (asset == null)
                    return;

                Selection.activeObject = asset;
                EditorGUIUtility.PingObject(asset);
            });

            row.Add(new Image { image = AssetDatabase.GetCachedIcon(path), pickingMode = PickingMode.Ignore });
            row.Add(new Label(path) { pickingMode = PickingMode.Ignore });
            return row;
        }

        static VisualElement CreateListBox(IReadOnlyCollection<string> lines, int limit)
        {
            var box = new VisualElement();
            box.AddToClassList("evo-well");
            box.AddToClassList("migration-list-box");
            foreach (string line in lines.Take(limit))
                box.Add(MigrationUI.Selectable(new Label(line)));

            if (lines.Count > limit)
                box.Add(MigrationUI.CreateDescription($"…and {lines.Count - limit:N0} more."));

            return box;
        }
        #endregion
    }
}
