// Asset Naming Manager — the Report tab: filters, the findings list, details, and the fix flow.
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;

namespace AssetNamingManager.UI
{
    public partial class NamingManagerWindow
    {
        const float RowHeight = 40f;

        /// <summary>One line in the list: either a group header or a finding.</summary>
        class ReportRow
        {
            public bool isHeader;
            public string header;
            public int count;
            public int fixable;
            public AssetNamingViolation violation;
        }

        readonly List<ReportRow> _rows = new List<ReportRow>();

        ListView _list;
        VisualElement _detailsPane;
        VisualElement _selectionBar;
        Label _selectionText;
        VisualElement _undoBar;
        Label _undoText;
        string _lastBatchId;

        // ================================================================ tab

        VisualElement BuildReportTab()
        {
            var tab = new VisualElement();
            tab.style.flexGrow = 1f;

            if (_ruleSet == null)   { tab.Add(BuildEmptyNoRuleSet());  return tab; }
            if (_report == null)    { tab.Add(BuildToolbar()); tab.Add(BuildEmptyNeverScanned()); return tab; }

            tab.Add(BuildToolbar());
            tab.Add(BuildSelectionBar());

            RebuildRows();

            if (_report.violations.Count == 0)
            {
                tab.Add(BuildEmptyClean());
                tab.Add(BuildUndoBar());
                return tab;
            }

            if (_rows.Count == 0)
            {
                tab.Add(BuildEmptyFiltered());
                tab.Add(BuildUndoBar());
                return tab;
            }

            var body = new VisualElement();
            body.style.flexDirection = FlexDirection.Row;
            body.style.flexGrow = 1f;
            body.style.overflow = Overflow.Hidden;

            body.Add(BuildList());
            body.Add(BuildDetailsPane());
            tab.Add(body);

            tab.Add(BuildUndoBar());
            UpdateSelectionBar();
            return tab;
        }

        // ================================================================ toolbar

        VisualElement BuildToolbar()
        {
            var bar = new VisualElement();
            bar.AddToClassList("anm-toolbar");

            var search = new ToolbarSearchField();
            search.AddToClassList("anm-search");
            search.value = _search;
            search.tooltip = "Filter by name, path, rule or message.";
            search.RegisterValueChangedCallback(evt =>
            {
                _search = evt.newValue ?? string.Empty;
                RefreshList();
            });
            bar.Add(search);

            var chips = new VisualElement();
            chips.AddToClassList("anm-chips");
            chips.Add(BuildSeverityChip(NamingSeverity.Error, 4));
            chips.Add(BuildSeverityChip(NamingSeverity.Warning, 2));
            chips.Add(BuildSeverityChip(NamingSeverity.Info, 1));
            bar.Add(chips);

            bar.Add(Spacer());

            var group = new EnumField("Group by", _group);
            group.AddToClassList("anm-group-field");
            group.RegisterValueChangedCallback(evt =>
            {
                _group = (GroupMode)evt.newValue;
                NamingManagerSettings.GroupMode = _group;
                RefreshList();
            });
            bar.Add(group);

            bar.Add(MakeButton("Select fixable", SelectAllFixable,
                "Tick every finding the Cop can correct on its own."));
            bar.Add(MakeButton("Export", ShowExportMenu, "Write the report to disk."));

            return bar;
        }

        Button BuildSeverityChip(NamingSeverity severity, int bit)
        {
            int count = 0;
            if (_report != null)
            {
                switch (severity)
                {
                    case NamingSeverity.Error:   count = _report.ErrorCount;   break;
                    case NamingSeverity.Warning: count = _report.WarningCount; break;
                    default:                     count = _report.InfoCount;    break;
                }
            }

            string plural = severity == NamingSeverity.Info ? "Info" : severity + "s";
            var chip = new Button { text = count + " " + plural };
            chip.AddToClassList("anm-chip");
            chip.AddToClassList("anm-chip--" + NamingManagerTheme.ClassFor(severity));
            chip.EnableInClassList("anm-chip--on", (_severityMask & bit) != 0);
            chip.tooltip = "Show or hide " + severity + " findings.";

            chip.clicked += () =>
            {
                _severityMask ^= bit;
                NamingManagerSettings.SeverityMask = _severityMask;
                chip.EnableInClassList("anm-chip--on", (_severityMask & bit) != 0);
                RefreshList();
            };

            return chip;
        }

        // ================================================================ rows

        void RebuildRows()
        {
            _rows.Clear();
            if (_report == null) return;

            var matched = new List<AssetNamingViolation>();
            string needle = _search.Trim();

            foreach (var v in _report.violations)
            {
                int bit = v.severity == NamingSeverity.Error ? 4 : v.severity == NamingSeverity.Warning ? 2 : 1;
                if ((_severityMask & bit) == 0) continue;
                if (needle.Length > 0 && !Matches(v, needle)) continue;
                matched.Add(v);
            }

            if (_group == GroupMode.None)
            {
                foreach (var v in matched) _rows.Add(new ReportRow { violation = v });
                return;
            }

            var buckets = new Dictionary<string, List<AssetNamingViolation>>(StringComparer.Ordinal);
            var order = new List<string>();

            foreach (var v in matched)
            {
                string key = GroupKey(v);
                List<AssetNamingViolation> bucket;
                if (!buckets.TryGetValue(key, out bucket))
                {
                    bucket = new List<AssetNamingViolation>();
                    buckets[key] = bucket;
                    order.Add(key);
                }
                bucket.Add(v);
            }

            order.Sort(StringComparer.Ordinal);

            foreach (var key in order)
            {
                var bucket = buckets[key];
                int fixable = 0;
                foreach (var v in bucket) if (v.CanAutoFix) fixable++;

                _rows.Add(new ReportRow { isHeader = true, header = key, count = bucket.Count, fixable = fixable });
                foreach (var v in bucket) _rows.Add(new ReportRow { violation = v });
            }
        }

        string GroupKey(AssetNamingViolation v)
        {
            switch (_group)
            {
                case GroupMode.Rule:      return string.IsNullOrEmpty(v.ruleName) ? "Project-wide" : v.ruleName;
                case GroupMode.Folder:    return string.IsNullOrEmpty(v.Directory) ? "(root)" : v.Directory;
                case GroupMode.AssetType: return string.IsNullOrEmpty(v.typeName) ? "Other" : v.typeName;
                case GroupMode.Severity:  return v.severity.ToString();
                case GroupMode.Problem:   return v.KindLabel;
                default:                  return string.Empty;
            }
        }

        static bool Matches(AssetNamingViolation v, string needle)
        {
            return Contains(v.assetPath, needle) || Contains(v.message, needle) ||
                   Contains(v.ruleName, needle) || Contains(v.KindLabel, needle) ||
                   Contains(v.suggestedName, needle);
        }

        static bool Contains(string haystack, string needle)
        {
            return !string.IsNullOrEmpty(haystack) &&
                   haystack.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0;
        }

        // ================================================================ list

        VisualElement BuildList()
        {
            _list = new ListView
            {
                itemsSource = _rows,
                fixedItemHeight = RowHeight,
                virtualizationMethod = CollectionVirtualizationMethod.FixedHeight,
                selectionType = SelectionType.Single,
                showBorder = false,
                makeItem = MakeRow,
                bindItem = BindRow
            };
            _list.AddToClassList("anm-list");
            _list.style.flexGrow = 1f;

            _list.selectionChanged += objects =>
            {
                foreach (var obj in objects)
                {
                    var row = obj as ReportRow;
                    if (row == null || row.isHeader) continue;
                    _selected = row.violation;
                    PingAsset(row.violation);
                    RefreshDetails();
                    return;
                }
            };

            _list.itemsChosen += objects =>
            {
                foreach (var obj in objects)
                {
                    var row = obj as ReportRow;
                    if (row == null || row.isHeader) continue;
                    var asset = AssetDatabase.LoadMainAssetAtPath(row.violation.assetPath);
                    if (asset != null) AssetDatabase.OpenAsset(asset);
                    return;
                }
            };

            return _list;
        }

        // makeItem is not index-aware, so every item holds BOTH shapes and bindItem picks one.
        static VisualElement MakeRow()
        {
            var container = new VisualElement();
            container.style.flexGrow = 1f;

            // --- group header
            var header = new VisualElement { name = "header" };
            header.AddToClassList("anm-group-header");
            var headerLabel = new Label { name = "headerLabel" };
            headerLabel.AddToClassList("anm-group-header__label");
            var headerCount = new Label { name = "headerCount" };
            headerCount.AddToClassList("anm-group-header__count");
            header.Add(headerLabel);
            header.Add(headerCount);
            container.Add(header);

            // --- finding row
            var row = new VisualElement { name = "row" };
            row.AddToClassList("anm-row");

            var stripe = new VisualElement { name = "stripe" };
            stripe.AddToClassList("anm-row__stripe");
            row.Add(stripe);

            var check = new Toggle { name = "check" };
            check.AddToClassList("anm-row__check");
            row.Add(check);

            var icon = new Image { name = "icon", scaleMode = ScaleMode.ScaleToFit };
            icon.AddToClassList("anm-row__icon");
            row.Add(icon);

            var main = new VisualElement();
            main.AddToClassList("anm-row__main");

            // One rich-text label, so the before and after truncate together instead of separately.
            var nameLabel = new Label { name = "name", enableRichText = true };
            nameLabel.AddToClassList("anm-row__name");
            main.Add(nameLabel);

            var line2 = new VisualElement();
            line2.AddToClassList("anm-row__line2");
            var message = new Label { name = "message" };
            message.AddToClassList("anm-row__message");
            var path = new Label { name = "path" };
            path.AddToClassList("anm-row__path");
            line2.Add(message);
            line2.Add(path);
            main.Add(line2);

            row.Add(main);

            var tag = new Label { name = "tag" };
            tag.AddToClassList("anm-row__tag");
            row.Add(tag);

            var pill = new Label { name = "pill" };
            pill.AddToClassList("anm-pill");
            row.Add(pill);

            container.Add(row);
            return container;
        }

        void BindRow(VisualElement element, int index)
        {
            if (index < 0 || index >= _rows.Count) return;
            var data = _rows[index];

            var header = element.Q<VisualElement>("header");
            var row = element.Q<VisualElement>("row");

            if (data.isHeader)
            {
                header.style.display = DisplayStyle.Flex;
                row.style.display = DisplayStyle.None;

                element.Q<Label>("headerLabel").text = data.header.ToUpperInvariant();
                element.Q<Label>("headerCount").text = data.fixable > 0
                    ? data.count + " findings · " + data.fixable + " fixable"
                    : data.count + " findings";
                return;
            }

            header.style.display = DisplayStyle.None;
            row.style.display = DisplayStyle.Flex;

            var v = data.violation;

            var stripe = element.Q<VisualElement>("stripe");
            stripe.RemoveFromClassList("anm-row__stripe--error");
            stripe.RemoveFromClassList("anm-row__stripe--warning");
            stripe.RemoveFromClassList("anm-row__stripe--info");
            stripe.AddToClassList("anm-row__stripe--" + NamingManagerTheme.ClassFor(v.severity));

            var check = element.Q<Toggle>("check");
            check.SetValueWithoutNotify(_checked.Contains(v));
            check.SetEnabled(v.CanAutoFix);
            check.tooltip = v.CanAutoFix ? "Include this in the next fix." : "This one has to be fixed by hand.";
            check.userData = v;
            check.RegisterValueChangedCallback(OnCheckChanged);

            element.Q<Image>("icon").image = NamingManagerTheme.AssetIcon(v.assetPath);

            string current = v.assetName + v.extension;
            var nameLabel = element.Q<Label>("name");
            if (v.CanAutoFix)
            {
                string after = NamingScanner.GetFileName(v.ResolvedPath);
                nameLabel.text = current +
                                 "  <color=#" + NamingManagerTheme.ToHex(NamingManagerTheme.Muted) + ">→</color>  " +
                                 NamingManagerTheme.HighlightDiff(current, after);
            }
            else
            {
                nameLabel.text = current +
                                 "  <color=#" + NamingManagerTheme.ToHex(NamingManagerTheme.Muted) + ">→  fix by hand</color>";
            }

            element.Q<Label>("message").text = v.message;
            element.Q<Label>("path").text = v.Directory;

            var tag = element.Q<Label>("tag");
            tag.text = v.KindLabel;

            var pill = element.Q<Label>("pill");
            pill.text = v.severity.ToString();
            pill.RemoveFromClassList("anm-pill--error");
            pill.RemoveFromClassList("anm-pill--warning");
            pill.RemoveFromClassList("anm-pill--info");
            pill.AddToClassList("anm-pill--" + NamingManagerTheme.ClassFor(v.severity));

            row.EnableInClassList("anm-row--checked", _checked.Contains(v));
        }

        void OnCheckChanged(ChangeEvent<bool> evt)
        {
            var toggle = evt.target as Toggle;
            if (toggle == null) return;

            var v = toggle.userData as AssetNamingViolation;
            if (v == null) return;

            if (evt.newValue) _checked.Add(v);
            else _checked.Remove(v);

            var row = toggle.parent;
            if (row != null) row.EnableInClassList("anm-row--checked", evt.newValue);

            UpdateSelectionBar();
        }

        void RefreshList()
        {
            RebuildRows();

            if (_list == null || _rows.Count == 0 || (_report != null && _report.violations.Count == 0))
            {
                ShowTab(Tab.Report);
                return;
            }

            _list.itemsSource = _rows;
            _list.Rebuild();
            UpdateSelectionBar();
        }

        static void PingAsset(AssetNamingViolation v)
        {
            var asset = AssetDatabase.LoadMainAssetAtPath(v.assetPath);
            if (asset != null) EditorGUIUtility.PingObject(asset);
        }

        // ================================================================ selection bar

        VisualElement BuildSelectionBar()
        {
            _selectionBar = new VisualElement();
            _selectionBar.AddToClassList("anm-selection-bar");
            _selectionBar.style.display = DisplayStyle.None;

            _selectionText = new Label();
            _selectionText.AddToClassList("anm-selection-bar__text");
            _selectionBar.Add(_selectionText);

            _selectionBar.Add(MakeButton("Clear", () =>
            {
                _checked.Clear();
                RefreshList();
            }));

            _selectionBar.Add(MakeButton("Preview changes", ShowPreflightSheet,
                "See every rename and move before anything is written.", true));

            return _selectionBar;
        }

        void UpdateSelectionBar()
        {
            if (_selectionBar == null) return;

            if (_checked.Count == 0)
            {
                _selectionBar.style.display = DisplayStyle.None;
                return;
            }

            var plans = NamingFixer.BuildPlans(_checked);
            int renames = 0, moves = 0;
            var folders = new HashSet<string>(StringComparer.Ordinal);

            foreach (var plan in plans)
            {
                string targetFolder = plan.targetFolder ?? FixPlan.ParentFolder(plan.sourcePath);
                if (!string.Equals(targetFolder, FixPlan.ParentFolder(plan.sourcePath), StringComparison.Ordinal))
                {
                    moves++;
                    if (!AssetDatabase.IsValidFolder(targetFolder)) folders.Add(targetFolder);
                }
                else renames++;
            }

            _selectionBar.style.display = DisplayStyle.Flex;
            _selectionText.text = string.Format(
                "<b>{0} selected</b> — {1} renames, {2} moves{3}. Nothing outside this list is touched.",
                _checked.Count, renames, moves,
                folders.Count > 0 ? ", " + folders.Count + " folders created" : string.Empty);
            _selectionText.enableRichText = true;
        }

        void SelectAllFixable()
        {
            _checked.Clear();
            foreach (var row in _rows)
            {
                if (row.isHeader || row.violation == null) continue;
                if (row.violation.CanAutoFix) _checked.Add(row.violation);
            }
            RefreshList();
            SetStatus(_checked.Count + " findings ticked. Nothing has changed yet.");
        }

        // ================================================================ details

        VisualElement BuildDetailsPane()
        {
            _detailsPane = new ScrollView();
            _detailsPane.AddToClassList("anm-details");
            RefreshDetails();
            return _detailsPane;
        }

        void RefreshDetails()
        {
            if (_detailsPane == null) return;

            var body = _detailsPane is ScrollView ? ((ScrollView)_detailsPane).contentContainer : _detailsPane;
            body.Clear();

            if (_selected == null)
            {
                var hint = new Label("Select a finding to see why it was flagged and what the fix would be.");
                hint.AddToClassList("anm-details__body");
                hint.AddToClassList("anm-text-muted");
                body.Add(hint);
                return;
            }

            var v = _selected;

            body.Add(Caption(v.assetName + v.extension, "anm-details__title"));
            body.Add(Caption(v.assetPath, "anm-details__path"));

            body.Add(Caption("WHY THIS IS FLAGGED", "anm-details__label"));
            body.Add(Caption(v.message, "anm-details__body"));

            if (!string.IsNullOrEmpty(v.detail))
            {
                var detail = Caption(v.detail, "anm-details__body");
                detail.AddToClassList("anm-text-muted");
                detail.style.marginTop = 4f;
                body.Add(detail);
            }

            if (v.CanAutoFix)
            {
                var section = new VisualElement();
                section.AddToClassList("anm-details__section");
                section.Add(Caption("PROPOSED CHANGE", "anm-details__label"));

                var box = new VisualElement();
                box.AddToClassList("anm-details__fix");

                var oldName = Caption(v.assetName + v.extension, "anm-details__fix-old");
                var arrow = new Label(" → ");
                arrow.AddToClassList("anm-text-muted");
                var newName = Caption(NamingScanner.GetFileName(v.ResolvedPath), "anm-details__fix-new");

                box.Add(oldName);
                box.Add(arrow);
                box.Add(newName);
                section.Add(box);

                var note = Caption(
                    "Applied with AssetDatabase.MoveAsset. The GUID does not change, so every prefab and " +
                    "scene reference follows the file.", "anm-details__body");
                note.AddToClassList("anm-text-muted");
                note.style.marginTop = 6f;
                section.Add(note);

                body.Add(section);
            }

            if (!string.IsNullOrEmpty(v.ruleName))
            {
                var section = new VisualElement();
                section.AddToClassList("anm-details__section");
                section.Add(Caption("RULE", "anm-details__label"));
                section.Add(Caption(v.ruleName + " · " + v.KindLabel + " · " + v.severity, "anm-details__body"));
                body.Add(section);
            }

            var actions = new VisualElement();
            actions.AddToClassList("anm-details__actions");
            actions.Add(MakeButton("Ping", () => PingAsset(v)));
            actions.Add(MakeButton("Open", () =>
            {
                var asset = AssetDatabase.LoadMainAssetAtPath(v.assetPath);
                if (asset != null) AssetDatabase.OpenAsset(asset);
            }));

            if (v.CanAutoFix)
            {
                actions.Add(MakeButton("Fix this one", () =>
                {
                    ApplyFixes(new List<AssetNamingViolation> { v });
                }, null, true));
            }

            body.Add(actions);
        }

        // ================================================================ fixing

        void ShowPreflightSheet()
        {
            var plans = NamingFixer.BuildPlans(_checked);
            if (plans.Count == 0)
            {
                SetStatus("Nothing in the selection would actually change.");
                return;
            }

            var sheet = new VisualElement();
            sheet.AddToClassList("anm-sheet");

            var head = new VisualElement();
            head.AddToClassList("anm-sheet__head");
            head.Add(Caption("Apply " + plans.Count + " changes?", "anm-sheet__title"));
            head.Add(Caption(
                "Every change below goes through AssetDatabase.MoveAsset, so references survive. " +
                "The whole batch is journalled and can be reverted afterwards.",
                "anm-sheet__sub"));
            sheet.Add(head);

            var list = new ScrollView();
            list.AddToClassList("anm-sheet__list");

            foreach (var plan in plans)
            {
                var row = new VisualElement();
                row.AddToClassList("anm-sheet__row");

                var label = new Label
                {
                    enableRichText = true,
                    text = NamingScanner.GetFileName(plan.sourcePath) +
                           "  <color=#" + NamingManagerTheme.ToHex(NamingManagerTheme.Muted) + ">→</color>  " +
                           NamingManagerTheme.HighlightDiff(
                               NamingScanner.GetFileName(plan.sourcePath),
                               NamingScanner.GetFileName(plan.TargetPath))
                };
                label.style.flexGrow = 1f;
                row.Add(label);

                bool moved = !string.Equals(FixPlan.ParentFolder(plan.sourcePath),
                                            plan.targetFolder ?? FixPlan.ParentFolder(plan.sourcePath),
                                            StringComparison.Ordinal);
                if (moved)
                {
                    var tag = new Label("move");
                    tag.AddToClassList("anm-row__tag");
                    row.Add(tag);
                }

                list.Add(row);
            }

            sheet.Add(list);

            var foot = new VisualElement();
            foot.AddToClassList("anm-sheet__foot");
            foot.Add(Spacer());
            foot.Add(MakeButton("Cancel", CloseSheet));
            foot.Add(MakeButton("Apply " + plans.Count + " changes", () =>
            {
                CloseSheet();
                ApplyPlans(plans);
            }, null, true));
            sheet.Add(foot);

            OpenSheet(sheet);
        }

        void ApplyFixes(IEnumerable<AssetNamingViolation> violations)
        {
            ApplyPlans(NamingFixer.BuildPlans(violations));
        }

        void ApplyPlans(List<FixPlan> plans)
        {
            if (plans == null || plans.Count == 0) return;

            var result = NamingFixer.Apply(plans);
            _lastBatchId = result.batchId;

            if (result.problems.Count > 0) ShowResultSheet(result);

            SetStatus(result.Summary());
            Scan();

            if (!string.IsNullOrEmpty(_lastBatchId)) ShowUndoBar(result);
        }

        void ShowResultSheet(FixResult result)
        {
            var sheet = new VisualElement();
            sheet.AddToClassList("anm-sheet");

            var head = new VisualElement();
            head.AddToClassList("anm-sheet__head");
            head.Add(Caption(result.Changed + " applied, " + result.problems.Count + " could not be", "anm-sheet__title"));
            head.Add(Caption(
                "Locked files, read-only source control and missing folders all land here. " +
                "Everything else in the batch went through.", "anm-sheet__sub"));
            sheet.Add(head);

            var list = new ScrollView();
            list.AddToClassList("anm-sheet__list");
            foreach (var problem in result.problems)
            {
                var row = new VisualElement();
                row.AddToClassList("anm-sheet__row");
                row.AddToClassList("anm-sheet__row--fail");
                row.Add(new Label(problem));
                list.Add(row);
            }
            sheet.Add(list);

            var foot = new VisualElement();
            foot.AddToClassList("anm-sheet__foot");
            foot.Add(Spacer());
            foot.Add(MakeButton("Close", CloseSheet, null, true));
            sheet.Add(foot);

            OpenSheet(sheet);
        }

        void OpenSheet(VisualElement sheet)
        {
            if (_sheetHost == null) return;
            _sheetHost.Clear();
            _sheetHost.Add(sheet);
            _sheetHost.style.display = DisplayStyle.Flex;
        }

        void CloseSheet()
        {
            if (_sheetHost == null) return;
            _sheetHost.Clear();
            _sheetHost.style.display = DisplayStyle.None;
        }

        // ================================================================ undo bar

        VisualElement BuildUndoBar()
        {
            _undoBar = new VisualElement();
            _undoBar.AddToClassList("anm-undo-bar");
            _undoBar.style.display = DisplayStyle.None;

            _undoText = new Label();
            _undoText.AddToClassList("anm-undo-bar__text");
            _undoBar.Add(_undoText);

            _undoBar.Add(MakeButton("Revert that batch", () =>
            {
                var batch = FixJournal.Find(_lastBatchId);
                if (batch == null) { SetStatus("That batch is no longer in the journal."); return; }

                var problems = new List<string>();
                int restored = FixJournal.Revert(batch, problems);
                _lastBatchId = null;

                SetStatus(restored + " assets put back" +
                          (problems.Count > 0 ? ", " + problems.Count + " could not be" : "") + ".");
                Scan();
            }));

            return _undoBar;
        }

        void ShowUndoBar(FixResult result)
        {
            if (_undoBar == null) return;
            _undoBar.style.display = DisplayStyle.Flex;
            _undoText.text = result.Summary() + " Unity's own undo does not cover asset moves, so use this.";
        }

        // ================================================================ empty states

        VisualElement BuildEmpty(string mark, string title, string body, string modifier)
        {
            var empty = new VisualElement();
            empty.AddToClassList("anm-empty");
            if (!string.IsNullOrEmpty(modifier)) empty.AddToClassList(modifier);

            var badge = new VisualElement();
            badge.AddToClassList("anm-empty__mark");
            var icon = NamingManagerTheme.Icon(mark);
            if (icon != null)
            {
                var image = new Image { image = icon, scaleMode = ScaleMode.ScaleToFit };
                image.AddToClassList("anm-empty__icon");
                badge.Add(image);
            }
            empty.Add(badge);

            empty.Add(Caption(title, "anm-empty__title"));
            empty.Add(Caption(body, "anm-empty__body"));
            return empty;
        }

        VisualElement BuildEmptyNoRuleSet()
        {
            var empty = BuildEmpty("d_Settings", "No rule set yet",
                "A rule set is where your naming convention lives. Commit it and the whole team shares " +
                "one convention instead of one person remembering it. Pick a preset to start — you can " +
                "change every rule afterwards.", null);

            var actions = new VisualElement();
            actions.AddToClassList("anm-empty__actions");

            foreach (NamingPreset preset in System.Enum.GetValues(typeof(NamingPreset)))
            {
                var value = preset;
                var button = MakeButton(ObjectNames.NicifyVariableName(value.ToString()),
                    () => CreateRuleSet(value), NamingRulePresets.Describe(value),
                    value == NamingPreset.UnityStandard);
                actions.Add(button);
            }

            empty.Add(actions);
            return empty;
        }

        void CreateRuleSet(NamingPreset preset)
        {
            var created = NamingManagerSettings.CreateRuleSet(preset);
            if (created == null) return;

            _ruleSet = created;
            if (_ruleSetField != null) _ruleSetField.SetValueWithoutNotify(created);
            SetStatus("Created " + created.name + " using the " + preset + " preset.");
            Scan();
        }

        VisualElement BuildEmptyNeverScanned()
        {
            var empty = BuildEmpty("d_Search Icon", "Nothing scanned yet",
                "Scanning is read-only. It never renames, moves or deletes anything — it only tells you " +
                "what it found. Press Scan when you are ready.", null);

            var actions = new VisualElement();
            actions.AddToClassList("anm-empty__actions");
            actions.Add(MakeButton("Scan", Scan, null, true));
            empty.Add(actions);
            return empty;
        }

        VisualElement BuildEmptyClean()
        {
            string detail = _report == null
                ? string.Empty
                : string.Format("Scanned {0} assets and {1} folders in {2:0.00}s against {3}.",
                    _report.assetsScanned, _report.foldersScanned, _report.durationSeconds, _report.ruleSetName);

            var empty = BuildEmpty("d_Progress", "Nothing to report",
                "Every asset in the scanned folders matches the convention. " + detail, "anm-empty--good");

            var actions = new VisualElement();
            actions.AddToClassList("anm-empty__actions");
            actions.Add(MakeButton("Export this result", ShowExportMenu));
            empty.Add(actions);
            return empty;
        }

        VisualElement BuildEmptyFiltered()
        {
            var empty = BuildEmpty("d_FilterByLabel", "No findings match your filters",
                "There are " + (_report != null ? _report.violations.Count : 0) +
                " findings in this report, but none of them match the current search and severity filters.",
                "anm-empty--neutral");

            var actions = new VisualElement();
            actions.AddToClassList("anm-empty__actions");
            actions.Add(MakeButton("Clear filters", () =>
            {
                _search = string.Empty;
                _severityMask = 7;
                NamingManagerSettings.SeverityMask = _severityMask;
                ShowTab(Tab.Report);
            }, null, true));
            empty.Add(actions);
            return empty;
        }

        // ================================================================ export

        void ShowExportMenu()
        {
            if (_report == null) { SetStatus("Scan first, then export."); return; }

            var menu = new GenericMenu();
            AddExportItem(menu, "HTML report (self-contained)", ReportFormat.Html);
            AddExportItem(menu, "CSV (spreadsheet)", ReportFormat.Csv);
            AddExportItem(menu, "JSON (scripting, diffable)", ReportFormat.Json);
            AddExportItem(menu, "Markdown (pull request)", ReportFormat.Markdown);
            menu.ShowAsContext();
        }

        void AddExportItem(GenericMenu menu, string label, ReportFormat format)
        {
            menu.AddItem(new GUIContent(label), false, () => Export(format));
        }

        void Export(ReportFormat format)
        {
            string folder = NamingManagerSettings.LastExportFolder;
            if (string.IsNullOrEmpty(folder)) folder = System.IO.Directory.GetCurrentDirectory();

            string path = EditorUtility.SaveFilePanel(
                "Export naming report", folder,
                ReportExporter.SuggestFileName(format, System.DateTime.Now),
                ReportExporter.ExtensionFor(format).TrimStart('.'));

            if (string.IsNullOrEmpty(path)) return;

            try
            {
                string written = ReportExporter.Write(_report, format, path);
                NamingManagerSettings.LastExportFolder = System.IO.Path.GetDirectoryName(written);
                SetStatus("Report written to " + written);
                EditorUtility.RevealInFinder(written);
            }
            catch (System.Exception e)
            {
                Debug.LogError(NamingManagerAbout.LogPrefix + "Could not write the report: " + e.Message);
                SetStatus("Export failed: " + e.Message);
            }
        }
    }
}
