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

namespace Evo.UI.Migration
{
    // The asset list for reviewing in v1 and applying in v2: filters, the list with its check boxes, and the selected asset's details
    internal sealed partial class MigrationWindow
    {
        const string AllCategories = "All";

        // Categories as the helper writes them, with their names in the filter
        static readonly (string category, string name)[] Categories =
        {
            (AllCategories, "All"),
            ("Prefabs", "Prefabs"),
            ("Scenes", "Scenes"),
            ("Scripts", "Scripts"),
            ("Assets", "Other")
        };

        static readonly string[] Tints = { "evo-chip--accent", "evo-chip--success", "evo-chip--warning", "evo-chip--error" };

        [SerializeField] string categoryFilter = AllCategories;
        [SerializeField] bool reviewFilter;
        [SerializeField] string searchText = "";
        [SerializeField] string shownGuid;

        readonly List<MigrationChange> visibleChanges = new();
        ListView changeList;
        Toggle selectAll;
        Label listCount;
        Label emptyList;
        ScrollView detail;
        Button primaryButton;
        bool showAllChanges;

        bool Applying => version == InstalledVersion.V2;

        #region Page
        void BuildReviewPage()
        {
            page.Add(CreateReviewToolbar());

            var split = new TwoPaneSplitView(0, 420, TwoPaneSplitViewOrientation.Horizontal) { viewDataKey = "migration-review-split" };
            split.AddToClassList("migration-split");
            page.Add(split);

            var listPane = new VisualElement();
            listPane.AddToClassList("migration-list-pane");
            split.Add(listPane);

            var listHeader = new VisualElement();
            listHeader.AddToClassList("migration-list-header");
            listPane.Add(listHeader);

            selectAll = MigrationUI.CreateCheckBox("Select or clear every listed asset that can be migrated", SelectVisible);
            listHeader.Add(selectAll);

            listCount = new Label();
            listCount.AddToClassList("migration-list-header__label");
            listHeader.Add(listCount);

            changeList = new ListView(visibleChanges, 38, MakeChangeRow, BindChangeRow) { selectionType = SelectionType.Single };
            changeList.AddToClassList("migration-list");
            changeList.selectionChanged += _ => ShowChange(changeList.selectedItem as MigrationChange);
            changeList.RegisterCallback<KeyDownEvent>(OnListKeyDown, TrickleDown.TrickleDown);
            listPane.Add(changeList);

            emptyList = new Label("No assets match the filters.");
            emptyList.AddToClassList("migration-empty-list");
            listPane.Add(emptyList);

            var detailPane = new VisualElement();
            detailPane.AddToClassList("migration-detail-pane");
            split.Add(detailPane);

            detail = new ScrollView(ScrollViewMode.Vertical) { horizontalScrollerVisibility = ScrollerVisibility.Hidden };
            detail.AddToClassList("migration-scroll");
            detail.contentContainer.AddToClassList("migration-detail");
            detailPane.Add(detail);

            if (Applying)
            {
                primaryButton = MigrationUI.CreatePrimaryButton("Apply", () => Guard(ApplySelected));
                SetButtons(MigrationUI.CreateButton("Load Export…", () => Guard(ChooseExport), "Load a different export"), primaryButton);
            }
            else
            {
                primaryButton = MigrationUI.CreatePrimaryButton("Continue", () => GoTo(Step.Export));
                SetButtons(primaryButton);
            }

            RefreshList();
            ShowChange(manifest.Find(shownGuid));
        }

        VisualElement CreateReviewToolbar()
        {
            var toolbar = new VisualElement();
            toolbar.AddToClassList("migration-toolbar");

            var categories = new MigrationUI.SegmentedControl<string>(category =>
            {
                categoryFilter = category;
                RefreshList();
            });

            foreach (var (category, name) in Categories)
            {
                int count = category == AllCategories ? manifest.changes.Count : manifest.changes.Count(change => change.category == category);
                categories.AddOption(category, $"{name}  {count:N0}");
            }

            categories.SetValueWithoutNotify(categoryFilter);
            toolbar.Add(categories);

            int review = manifest.changes.Count(change => change.NeedsReview);
            var reviewButton = new Button { text = $"Needs Review  {review:N0}", tooltip = "Show only assets with manual review notes", focusable = false };
            reviewButton.AddToClassList("migration-filter");
            reviewButton.EnableInClassList("migration-filter--on", reviewFilter);
            reviewButton.clicked += () =>
            {
                reviewFilter = !reviewFilter;
                reviewButton.EnableInClassList("migration-filter--on", reviewFilter);
                RefreshList();
            };
            toolbar.Add(reviewButton);

            ToolbarSearchField search = MigrationUI.CreateSearchField(searchText, "Search paths, changes and review notes", text =>
            {
                searchText = text;
                RefreshList();
            });
            toolbar.Add(search);

            if (Applying)
                toolbar.Add(MigrationUI.CreateIconButton(MigrationUI.GetIcon("d_Refresh"), "Check the files again, as after restoring or editing them", () => Guard(Recheck)));

            return toolbar;
        }

        // Statuses come from the files, so a file restored or edited outside the window changes them.
        // Assets that became ready are selected, like on loading, and the rest keep their selection.
        void Recheck()
        {
            var wasSelectable = new HashSet<MigrationChange>(manifest.changes.Where(CanSelect));
            MigrationEngine.Preview(manifest);
            foreach (MigrationChange change in manifest.changes)
            {
                if (!CanSelect(change))
                    change.selected = false;
                else if (!wasSelectable.Contains(change))
                    change.selected = true;
            }

            MigrationEngine.DeselectStranded(manifest);
            Refresh();
        }
        #endregion

        #region List
        bool CanSelect(MigrationChange change) => change.HasChanges && (!Applying || change.status == ChangeStatus.Ready);

        bool Matches(MigrationChange change)
        {
            if (categoryFilter != AllCategories && change.category != categoryFilter)
                return false;

            if (reviewFilter && !change.NeedsReview)
                return false;

            if (string.IsNullOrEmpty(searchText))
                return true;

            return Contains(change.CurrentPath) || change.details.Any(Contains) || change.warnings.Any(Contains)
                || (Applying && Contains(MigrationEngine.Describe(change.status)));

            bool Contains(string text) => text != null && text.IndexOf(searchText, StringComparison.OrdinalIgnoreCase) >= 0;
        }

        void RefreshList()
        {
            if (changeList == null)
                return;

            visibleChanges.Clear();
            visibleChanges.AddRange(manifest.changes.Where(Matches));
            changeList.Rebuild();

            // Keep the shown asset selected while it's listed. Its details stay when a filter hides it.
            int index = visibleChanges.FindIndex(change => change.guid == shownGuid);
            changeList.SetSelectionWithoutNotify(index >= 0 ? new[] { index } : Array.Empty<int>());

            bool empty = visibleChanges.Count == 0;
            changeList.style.display = empty ? DisplayStyle.None : DisplayStyle.Flex;
            emptyList.style.display = empty ? DisplayStyle.Flex : DisplayStyle.None;
            emptyList.text = manifest.changes.Count == 0 ? "Nothing here uses Evo UI v1, so there's nothing to migrate." : "No assets match the filters.";
            UpdateSelectionState();
        }

        VisualElement MakeChangeRow()
        {
            var row = new VisualElement();
            row.AddToClassList("migration-row");

            // Not focusable, so Space reaches the list, which switches the selected row's box
            Toggle check = MigrationUI.CreateCheckBox(null, value =>
            {
                if (row.userData is MigrationChange change)
                    SetSelected(change, value);
            });
            check.name = "check";
            check.focusable = false;
            row.Add(check);

            var icon = new Image { name = "icon", pickingMode = PickingMode.Ignore };
            icon.AddToClassList("migration-row__icon");
            row.Add(icon);

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

            var name = new Label { name = "name", pickingMode = PickingMode.Ignore };
            name.AddToClassList("migration-row__name");
            text.Add(name);

            var folder = new Label { name = "folder", pickingMode = PickingMode.Ignore };
            folder.AddToClassList("migration-row__folder");
            text.Add(folder);

            Label review = MigrationUI.CreateChip("Review", "evo-chip--warning");
            review.name = "review";
            row.Add(review);

            Label state = MigrationUI.CreateChip("");
            state.name = "state";
            row.Add(state);

            return row;
        }

        void BindChangeRow(VisualElement row, int index)
        {
            MigrationChange change = visibleChanges[index];
            row.userData = change;

            bool selectable = CanSelect(change);
            var check = row.Q<Toggle>("check");
            check.SetEnabled(selectable);
            check.SetValueWithoutNotify(selectable && change.selected);
            check.tooltip = selectable ? null : !change.HasChanges ? "Nothing changes automatically, only review notes" : "Can't be applied: " + MigrationEngine.Describe(change.status);

            string path = change.CurrentPath;
            row.Q<Image>("icon").image = GetAssetIcon(change);
            row.Q<Label>("name").text = Path.GetFileName(path);
            row.Q<Label>("folder").text = Path.GetDirectoryName(path)?.Replace('\\', '/');
            row.Q("review").style.display = change.NeedsReview ? DisplayStyle.Flex : DisplayStyle.None;

            var (text, tint) = GetStatusChip(change);
            var state = row.Q<Label>("state");
            state.text = text;
            state.style.display = string.IsNullOrEmpty(text) ? DisplayStyle.None : DisplayStyle.Flex;
            SetTint(state, tint);

            row.EnableInClassList("migration-row--unavailable", Applying && change.HasChanges && !selectable && change.status != ChangeStatus.Applied);
            row.tooltip = path;
        }

        (string text, string tint) GetStatusChip(MigrationChange change)
        {
            if (!Applying && !change.HasChanges)
                return ("Review only", null);

            if (!Applying)
                return (change.details.Count > 0 ? MigrationUI.Plural(change.details.Count, "change") : "Changes", null);

            return change.status switch
            {
                ChangeStatus.Ready => ("Ready", "evo-chip--accent"),
                ChangeStatus.Applied => ("Applied", "evo-chip--success"),
                ChangeStatus.ReviewOnly => ("Review only", null),
                ChangeStatus.Changed => ("Changed", "evo-chip--warning"),
                ChangeStatus.Missing => ("Missing", "evo-chip--error"),
                ChangeStatus.OutsideAssets => ("Outside Assets", "evo-chip--error"),
                ChangeStatus.PresetConflict => ("Conflict", "evo-chip--error"),
                _ => (null, null)
            };
        }

        static Texture GetAssetIcon(MigrationChange change)
        {
            Texture icon = AssetDatabase.GetCachedIcon(change.CurrentPath);
            if (icon != null)
                return icon;

            return MigrationUI.GetIcon(change.category switch
            {
                "Prefabs" => "Prefab Icon",
                "Scenes" => "SceneAsset Icon",
                "Scripts" => "cs Script Icon",
                _ => "ScriptableObject Icon"
            });
        }

        void OnListKeyDown(KeyDownEvent evt)
        {
            if (evt.keyCode == KeyCode.Escape && shownGuid != null)
            {
                ShowOverview();
                evt.StopPropagation();
                return;
            }

            if (evt.keyCode != KeyCode.Space || changeList.selectedItem is not MigrationChange change || !CanSelect(change))
                return;

            SetSelected(change, !change.selected);
            evt.StopPropagation();
        }
        #endregion

        #region Selection
        void SetSelected(MigrationChange change, bool value)
        {
            int others = Cascade(change, value);
            changeList.RefreshItems();
            UpdateSelectionState();

            if (others > 0)
                ShowNotification(new GUIContent(value ? $"Also selected {MigrationUI.Plural(others, "asset")} it needs" : $"Also cleared {MigrationUI.Plural(others, "asset")} that need it"), 1.5);
        }

        void SelectVisible(bool value)
        {
            foreach (MigrationChange change in visibleChanges.Where(CanSelect).ToList())
                Cascade(change, value);

            changeList.RefreshItems();
            UpdateSelectionState();
        }

        // Selecting a change selects the ones it needs, and clearing one clears the ones that need it, so a selection can always be applied.
        // Returns how many others changed.
        int Cascade(MigrationChange change, bool value)
        {
            int others = 0;
            var visited = new HashSet<MigrationChange>();
            var pending = new Stack<MigrationChange>();
            pending.Push(change);
            while (pending.Count > 0)
            {
                MigrationChange current = pending.Pop();
                if (!visited.Add(current) || !CanSelect(current))
                    continue;

                if (current.selected != value)
                {
                    current.selected = value;
                    if (current != change)
                        others++;
                }

                IEnumerable<MigrationChange> next = value ? MigrationEngine.GetDependencies(manifest, current) : MigrationEngine.GetDependents(manifest, current);
                foreach (MigrationChange other in next)
                    pending.Push(other);
            }

            return others;
        }

        void UpdateSelectionState()
        {
            List<MigrationChange> selectable = visibleChanges.Where(CanSelect).ToList();
            int visibleSelected = selectable.Count(change => change.selected);
            selectAll.SetEnabled(selectable.Count > 0);
            selectAll.SetValueWithoutNotify(selectable.Count > 0 && visibleSelected == selectable.Count);
            selectAll.showMixedValue = visibleSelected > 0 && visibleSelected < selectable.Count;

            int total = manifest.changes.Count;
            listCount.text = visibleChanges.Count == total ? MigrationUI.Plural(total, "asset") : $"{visibleChanges.Count:N0} of {MigrationUI.Plural(total, "asset")}";

            int selected = manifest.changes.Count(change => change.selected && CanSelect(change));
            if (Applying)
            {
                int ready = manifest.changes.Count(change => change.HasChanges && change.status == ChangeStatus.Ready);
                int kept = MigrationEngine.MissingKeptAssets(manifest).Count;
                string issue = MigrationEngine.SelectionIssue(manifest);
                if (issue != null)
                    SetStatus(issue, true);
                else if (ready == 0)
                    SetStatus(kept == 0 ? "Nothing left to apply." : $"Only the kept v1 assets are left to copy to {MigrationEngine.KeptFolder}.");
                else
                    SetStatus($"{selected:N0} of {MigrationUI.Plural(ready, "ready asset")} selected. Replaced files are backed up first.");

                primaryButton.text = selected == 0 && kept > 0 ? "Copy v1 Assets" : selected == 1 ? "Apply 1 Asset" : $"Apply {selected:N0} Assets";
                primaryButton.SetEnabled((selected > 0 || kept > 0) && issue == null);
            }
            else
            {
                int withChanges = manifest.changes.Count(change => change.HasChanges);
                SetStatus(withChanges == 0
                    ? "Nothing to migrate."
                    : $"{selected:N0} of {MigrationUI.Plural(withChanges, "asset")} with changes selected. Exporting doesn't change your project.");
                primaryButton.SetEnabled(selected > 0);
            }
        }
        #endregion

        #region Details
        void ShowChange(MigrationChange change)
        {
            // A long list of changes starts collapsed for each asset
            if (change?.guid != shownGuid)
                showAllChanges = false;

            shownGuid = change?.guid;
            detail.Clear();
            if (change == null)
            {
                BuildOverview();
                return;
            }

            var header = new VisualElement();
            header.AddToClassList("migration-detail__header");
            detail.Add(header);

            var icon = new Image { image = GetAssetIcon(change), pickingMode = PickingMode.Ignore };
            icon.AddToClassList("migration-detail__icon");
            header.Add(icon);
            header.Add(MigrationUI.CreateTitle(Path.GetFileName(change.CurrentPath)));
            header.Add(MigrationUI.CreateIconButton(MigrationUI.GetIcon("d_clear"), "Back to the overview (Esc)", ShowOverview));

            var path = new Label(change.CurrentPath) { tooltip = "Click to show it in the Project window." };
            path.AddToClassList("migration-detail__path");
            path.RegisterCallback<ClickEvent>(_ => PingAsset(change));
            detail.Add(path);

            var (text, type) = ExplainStatus(change);
            if (text != null)
                detail.Add(MigrationUI.CreateCallout(text, type));

            // Manual steps come first: an asset can have hundreds of automatic changes
            if (change.NeedsReview)
            {
                detail.Add(MigrationUI.CreateHeading($"Manual review ({change.warnings.Count})"));
                foreach (var (warning, count) in Group(change.warnings))
                    detail.Add(MigrationUI.CreateCallout(count > 1 ? $"{warning} (×{count})" : warning, CalloutType.Warning));
            }

            if (change.details.Count > 0)
                AddChangeLines(change);
            else if (!change.HasChanges)
                detail.Add(MigrationUI.CreateDescription("Nothing in this asset changes automatically."));

            AddRelatedChanges("Needs", "Migrated together with these, so they're selected with it.", MigrationEngine.GetDependencies(manifest, change).ToList());
            AddRelatedChanges("Needed by", "These need it, so they're cleared with it.", MigrationEngine.GetDependents(manifest, change).ToList());

            if (change.generated.Count > 0)
            {
                detail.Add(MigrationUI.CreateHeading("Generated presets"));
                detail.Add(MigrationUI.CreateDescription($"Copies of the animation presets it uses, written to {MigrationEngine.PresetsFolder}."));
                detail.Add(CreateListBox(change.generated.Select(generated => generated.path).ToList(), 50));
            }

            var buttons = new VisualElement();
            buttons.AddToClassList("migration-buttons");
            detail.Add(buttons);

            Button show = MigrationUI.CreateButton("Show in Project", () => PingAsset(change));
            show.SetEnabled(File.Exists(change.CurrentPath));
            buttons.Add(show);
        }

        // Identical lines, such as the same edit on many objects in a scene, show once with their count
        void AddChangeLines(MigrationChange change)
        {
            const int Limit = 30;
            List<(string text, int count)> lines = Group(change.details);
            detail.Add(MigrationUI.CreateHeading($"Changes ({change.details.Count})"));

            bool all = showAllChanges || lines.Count <= Limit;
            foreach (var (line, count) in all ? lines : lines.Take(Limit))
                detail.Add(CreateBullet(line, count));

            if (all)
                return;

            Button more = MigrationUI.CreateButton($"Show All {lines.Count:N0}", () =>
            {
                showAllChanges = true;
                ShowChange(change);
            });
            more.AddToClassList("migration-more");
            detail.Add(more);
        }

        static List<(string text, int count)> Group(IEnumerable<string> lines)
        {
            var groups = new List<(string text, int count)>();
            var indices = new Dictionary<string, int>();
            foreach (string line in lines)
            {
                if (indices.TryGetValue(line, out int index))
                    groups[index] = (line, groups[index].count + 1);
                else
                {
                    indices[line] = groups.Count;
                    groups.Add((line, 1));
                }
            }

            return groups;
        }

        (string text, CalloutType type) ExplainStatus(MigrationChange change)
        {
            if (!Applying)
                return (null, CalloutType.Info);

            return change.status switch
            {
                ChangeStatus.Applied => ("Already applied.", CalloutType.Success),
                ChangeStatus.Changed => ("This file changed since the export, so it isn't overwritten. Restore its v1 version, or export again from your v1 project.", CalloutType.Warning),
                ChangeStatus.Missing => ("This asset isn't in the project anymore, so there's nothing to migrate.", CalloutType.Error),
                ChangeStatus.OutsideAssets => ("This asset isn't under Assets anymore, so it isn't changed.", CalloutType.Error),
                ChangeStatus.PresetConflict => ($"A preset copy it needs conflicts with a different file in {MigrationEngine.PresetsFolder}.", CalloutType.Error),
                _ => (null, CalloutType.Info)
            };
        }

        void AddRelatedChanges(string title, string description, List<MigrationChange> changes)
        {
            if (changes.Count == 0)
                return;

            detail.Add(MigrationUI.CreateHeading($"{title} ({changes.Count})"));
            detail.Add(MigrationUI.CreateDescription(description));
            foreach (MigrationChange change in changes.Take(50))
            {
                var row = new VisualElement { tooltip = change.CurrentPath };
                row.AddToClassList("migration-link-row");
                row.RegisterCallback<ClickEvent>(_ => Reveal(change));
                row.Add(new Image { image = GetAssetIcon(change), pickingMode = PickingMode.Ignore });
                row.Add(new Label(change.CurrentPath) { pickingMode = PickingMode.Ignore });
                detail.Add(row);
            }

            if (changes.Count > 50)
                detail.Add(MigrationUI.CreateDescription($"…and {changes.Count - 50:N0} more."));
        }

        // Selects a change in the list, or only shows its details when the filters hide it
        void Reveal(MigrationChange change)
        {
            int index = visibleChanges.IndexOf(change);
            if (index < 0)
            {
                changeList.SetSelectionWithoutNotify(Array.Empty<int>());
                ShowChange(change);
                return;
            }

            changeList.SetSelection(index);
            changeList.ScrollToItem(index);
        }

        void ShowOverview()
        {
            changeList.SetSelectionWithoutNotify(Array.Empty<int>());
            ShowChange(null);
        }

        void BuildOverview()
        {
            detail.Add(MigrationUI.CreateTitle("Overview"));
            detail.Add(CreateLead($"{(Applying ? "Export" : "Analysis")} of {manifest.project}{FormatDate(manifest.createdUtc)}. "
                + "Select an asset to see its changes and any manual steps."));

            var stats = new VisualElement();
            stats.AddToClassList("migration-stats");
            detail.Add(stats);

            int review = manifest.changes.Count(change => change.NeedsReview);
            if (Applying)
            {
                int ready = manifest.changes.Count(change => change.HasChanges && change.status == ChangeStatus.Ready);
                int applied = manifest.changes.Count(change => change.status == ChangeStatus.Applied);
                int blocked = manifest.changes.Count(change => change.HasChanges && change.status != ChangeStatus.Ready && change.status != ChangeStatus.Applied);
                stats.Add(CreateStat(ready, "Ready"));
                stats.Add(CreateStat(applied, "Applied", applied > 0 ? "migration-stat--success" : null));
                stats.Add(CreateStat(blocked, "Can't apply", blocked > 0 ? "migration-stat--error" : null));
                stats.Add(CreateStat(review, "Need review", review > 0 ? "migration-stat--warning" : null));
            }
            else
            {
                stats.Add(CreateStat(manifest.changes.Count(change => change.HasChanges), "With changes"));
                stats.Add(CreateStat(manifest.changes.Count(change => !change.HasChanges), "Review only"));
                stats.Add(CreateStat(review, "Need review", review > 0 ? "migration-stat--warning" : null));
            }

            if (manifest.kept.Count > 0)
            {
                detail.Add(MigrationUI.CreateHeading($"Kept v1 assets ({manifest.kept.Count})"));
                detail.Add(MigrationUI.CreateDescription($"Evo UI v2 doesn't include these, so applying copies them to {MigrationEngine.KeptFolder} with their GUIDs "
                    + "and import settings. Your assets keep using them, and the sprite library stays available. Fonts, prefabs and demo sprites are kept only when "
                    + "your assets use them, and prefabs are migrated like yours."));
                detail.Add(CreateListBox(manifest.kept.Select(asset => asset.path.StartsWith(MigrationEngine.KeptFolder + "/", StringComparison.Ordinal)
                    ? asset.path.Substring(MigrationEngine.KeptFolder.Length + 1) : asset.path).ToList(), 200));
            }

            if (manifest.notes.Count > 0)
            {
                detail.Add(MigrationUI.CreateHeading($"Analysis notes ({manifest.notes.Count})"));
                detail.Add(MigrationUI.CreateDescription("Files that were skipped or only partly converted, and why."));
                detail.Add(CreateListBox(manifest.notes, 200));
            }
        }

        static string FormatDate(string utc)
        {
            return DateTime.TryParse(utc, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTime date)
                ? ", made " + date.ToLocalTime().ToString("g", CultureInfo.CurrentCulture)
                : "";
        }

        static VisualElement CreateBullet(string text, int count)
        {
            var bullet = new VisualElement();
            bullet.AddToClassList("migration-bullet");

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

            var label = MigrationUI.Selectable(new Label(text));
            label.AddToClassList("migration-bullet__text");
            bullet.Add(label);

            if (count > 1)
                bullet.Add(MigrationUI.CreateChip($"×{count}"));

            return bullet;
        }

        static void PingAsset(MigrationChange change)
        {
            var asset = AssetDatabase.LoadMainAssetAtPath(change.CurrentPath);
            if (asset == null)
                return;

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