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

namespace Evo.UI.Migration
{
    internal enum CalloutType { Info, Success, Warning, Error }

    /// <summary>
    /// Building blocks in the style of Evo's editor windows (EvoEditorUITK in Evo/Shared). The tool can't use Evo/Shared,
    /// which automatic migration removes, so these and MigrationWindow.uss carry their own copy of the parts it needs,
    /// with the same class names.
    /// </summary>
    internal static class MigrationUI
    {
        #region Theme
        /// <summary>
        /// Adds the colors for the editor skin and the window's stylesheet. Safe to call again.
        /// </summary>
        public static void Apply(VisualElement root, StyleSheet styleSheet)
        {
            root.AddToClassList("evo-window");
            root.EnableInClassList("evo-window--dark", EditorGUIUtility.isProSkin);
            root.EnableInClassList("evo-window--light", !EditorGUIUtility.isProSkin);

            if (styleSheet != null && !root.styleSheets.Contains(styleSheet))
                root.styleSheets.Add(styleSheet);
        }

        public static Texture2D GetIcon(string name) => EditorGUIUtility.IconContent(name).image as Texture2D;
        #endregion

        #region Text
        /// <summary>
        /// Large bold title at the top of a page.
        /// </summary>
        public static Label CreateTitle(string text)
        {
            var title = new Label(text);
            title.AddToClassList("evo-title");
            return title;
        }

        /// <summary>
        /// Small capitalized heading above a group of fields or a list.
        /// </summary>
        public static Label CreateHeading(string text)
        {
            var heading = new Label(text.ToUpperInvariant());
            heading.AddToClassList("evo-heading");
            return heading;
        }

        /// <summary>
        /// Small muted text, such as a hint or what an option does. Add evo-description--warning to flag a problem.
        /// </summary>
        public static Label CreateDescription(string text = null, string className = null)
        {
            var description = new Label(text ?? "");
            description.AddToClassList("evo-description");
            if (!string.IsNullOrEmpty(className))
                description.AddToClassList(className);

            return description;
        }

        /// <summary>
        /// Small pill with a word or a count. Add evo-chip--accent, --success, --warning or --error to tint it.
        /// </summary>
        public static Label CreateChip(string text, string className = null)
        {
            var chip = new Label(text) { pickingMode = PickingMode.Ignore };
            chip.AddToClassList("evo-chip");
            if (!string.IsNullOrEmpty(className))
                chip.AddToClassList(className);

            return chip;
        }

        /// <summary>
        /// Small accent text that opens a web page, such as the documentation.
        /// </summary>
        public static Label CreateLink(string text, string url)
        {
            var link = new Label(text) { tooltip = url };
            link.AddToClassList("evo-link");
            link.RegisterCallback<ClickEvent>(_ => Application.OpenURL(url));
            return link;
        }

        /// <summary>
        /// Text the user can select and copy, such as a path or an analysis note.
        /// </summary>
        public static T Selectable<T>(T label) where T : TextElement
        {
            label.selection.isSelectable = true;
            return label;
        }

        /// <summary>
        /// A count with its noun, such as "1 asset" or "1,024 assets".
        /// </summary>
        public static string Plural(int count, string noun) => count == 1 ? $"1 {noun}" : $"{count:N0} {noun}s";
        #endregion

        #region Buttons
        /// <summary>
        /// Button with a rounded fill that brightens and gains an outline on hover. Add evo-primary for the main action of a page.
        /// </summary>
        public static Button CreateButton(string text, Action onClick, string tooltip = null)
        {
            // Unity counts cut-off text as elided and swaps the tooltip for it, losing the one set here
            var button = new Button(onClick) { text = text, tooltip = tooltip, displayTooltipWhenElided = false };
            button.AddToClassList("evo-button");
            return button;
        }

        public static Button CreatePrimaryButton(string text, Action onClick, string tooltip = null)
        {
            Button button = CreateButton(text, onClick, tooltip);
            button.AddToClassList("evo-primary");
            return button;
        }

        /// <summary>
        /// Borderless button that shows only an icon, such as a close button. The tooltip says what it does.
        /// </summary>
        public static Button CreateIconButton(Texture icon, string tooltip, Action onClick)
        {
            var button = new Button(onClick) { tooltip = tooltip };
            button.AddToClassList("evo-icon-button");

            var image = new Image { image = icon, pickingMode = PickingMode.Ignore };
            image.AddToClassList("evo-icon-button__icon");
            button.Add(image);
            return button;
        }

        /// <summary>
        /// Borderless button with the editor's menu icon that opens a menu of actions.
        /// </summary>
        public static ToolbarMenu CreateMoreMenu(string tooltip)
        {
            var menu = new ToolbarMenu { tooltip = tooltip };
            menu.AddToClassList("evo-more-menu");

            var icon = new Image { image = GetIcon("_Menu"), pickingMode = PickingMode.Ignore };
            icon.AddToClassList("evo-more-menu__icon");
            menu.Add(icon);
            return menu;
        }
        #endregion

        #region Fields
        /// <summary>
        /// Full-width toggle row with the label on the left, and the state and a check box on the right. Clicking anywhere on the row switches it.
        /// </summary>
        public static Toggle CreateToggle(string label, bool value, string tooltip, Action<bool> onChanged)
        {
            Toggle toggle = CreateToggle(label, onChanged);
            toggle.SetValueWithoutNotify(value);
            toggle.tooltip = tooltip;

            // Both states are always there, and the stylesheet shows the one matching :checked
            var state = new VisualElement { pickingMode = PickingMode.Ignore };
            state.AddToClassList("evo-toggle__state");
            state.Add(CreateToggleState("On", "evo-toggle__on"));
            state.Add(CreateToggleState("Off", "evo-toggle__off"));
            VisualElement input = toggle.Q(className: Toggle.inputUssClassName);
            input.Insert(0, state);
            return toggle;
        }

        /// <summary>
        /// Only the check box, for list rows. showMixedValue draws a dash, as for a select-all box with some items selected.
        /// </summary>
        public static Toggle CreateCheckBox(string tooltip, Action<bool> onChanged)
        {
            Toggle toggle = CreateToggle(null, onChanged);
            toggle.tooltip = tooltip;
            toggle.AddToClassList("evo-toggle--box");
            return toggle;
        }

        static Toggle CreateToggle(string label, Action<bool> onChanged)
        {
            var toggle = new MixedToggle(label);
            toggle.AddToClassList("evo-toggle");

            // Evo's own box replaces the built-in check mark, whose image the editor theme swaps on hover and focus
            var box = new VisualElement { pickingMode = PickingMode.Ignore };
            box.AddToClassList("evo-toggle__box");
            toggle.Q(className: Toggle.inputUssClassName).Add(box);

            var check = new VisualElement { pickingMode = PickingMode.Ignore };
            check.style.backgroundImage = GetIcon("d_checkmark");
            check.AddToClassList("evo-toggle__check");
            box.Add(check);

            var dash = new VisualElement { pickingMode = PickingMode.Ignore };
            dash.AddToClassList("evo-toggle__dash");
            box.Add(dash);

            if (onChanged != null)
                toggle.RegisterValueChangedCallback(evt => onChanged(evt.newValue));

            return toggle;
        }

        static Label CreateToggleState(string text, string className)
        {
            var label = new Label(text) { pickingMode = PickingMode.Ignore };
            label.AddToClassList(className);
            return label;
        }

        // Unity marks mixed values only on its own check mark, which Evo's box replaces
        sealed class MixedToggle : Toggle
        {
            public MixedToggle(string label) : base(label) { }

            protected override void UpdateMixedValueContent()
            {
                base.UpdateMixedValueContent();
                EnableInClassList("evo-toggle--mixed", showMixedValue);
            }
        }

        /// <summary>
        /// The label and the editor's popup on a rounded fill. onChanged gets the chosen index.
        /// </summary>
        public static DropdownField CreateDropdown(string label, List<string> choices, int index, string tooltip, Action<int> onChanged)
        {
            var dropdown = new DropdownField(label, choices, index) { tooltip = tooltip };
            dropdown.AddToClassList("evo-dropdown");

            // Only the popup opens the menu, so it takes the pointer itself and lights up on its own
            VisualElement popup = dropdown.Q(className: DropdownField.inputUssClassName);
            popup.pickingMode = PickingMode.Position;
            popup.AddToClassList("evo-inverted");

            dropdown.RegisterValueChangedCallback(_ => onChanged(dropdown.index));
            return dropdown;
        }

        /// <summary>
        /// Text input on a rounded fill, with an outline on hover and the accent color while typing.
        /// </summary>
        public static TextField CreateTextField(string value, string tooltip, Action<string> onChanged)
        {
            var field = new TextField { value = value ?? "", tooltip = tooltip };
            field.AddToClassList("evo-text-field");
            field.RegisterValueChangedCallback(evt => onChanged(evt.newValue));
            return field;
        }

        /// <summary>
        /// Search field with a magnifier and a clear button on a rounded fill.
        /// </summary>
        public static ToolbarSearchField CreateSearchField(string value, string tooltip, Action<string> onChanged)
        {
            var field = new ToolbarSearchField { tooltip = tooltip, placeholderText = "Search" };
            field.SetValueWithoutNotify(value ?? "");
            field.AddToClassList("evo-search");

            // The magnifier only marks the field, so a click on it goes to the field
            var searchButton = field.Q<Button>(className: ToolbarSearchField.searchButtonUssClassName);
            searchButton.pickingMode = PickingMode.Ignore;
            searchButton.style.backgroundImage = GetIcon("d_SearchWindow");

            var cancelButton = field.Q<Button>(className: ToolbarSearchField.cancelButtonUssClassName);
            cancelButton.tooltip = "Clear";
            cancelButton.AddToClassList("evo-icon-button");
            var clearIcon = new Image { image = GetIcon("d_clear"), pickingMode = PickingMode.Ignore };
            clearIcon.AddToClassList("evo-icon-button__icon");
            cancelButton.Add(clearIcon);

            // Unity drops the placeholder's look only when text changes with a notification, so restored text would look like one
            if (!string.IsNullOrEmpty(value))
                field.Q(className: ToolbarSearchField.textInputUssClassName)?.RemoveFromClassList(TextField.placeholderUssClassName);

            field.RegisterValueChangedCallback(evt => onChanged(evt.newValue ?? ""));
            return field;
        }
        #endregion

        #region Callout
        /// <summary>
        /// An icon and wrapping text on a rounded fill, tinted by type. Change it later with SetCallout.
        /// </summary>
        public static VisualElement CreateCallout(string message, CalloutType type = CalloutType.Info)
        {
            var callout = new VisualElement();
            callout.AddToClassList("evo-callout");

            var icon = new Image { pickingMode = PickingMode.Ignore };
            icon.AddToClassList("evo-callout__icon");
            callout.Add(icon);

            var text = Selectable(new Label());
            text.AddToClassList("evo-callout__text");
            callout.Add(text);

            SetCallout(callout, message, type);
            return callout;
        }

        public static void SetCallout(VisualElement callout, string message, CalloutType type)
        {
            callout.Q<Label>(className: "evo-callout__text").text = message ?? "";
            callout.Q<Image>(className: "evo-callout__icon").image = GetIcon(type switch
            {
                CalloutType.Success => "TestPassed",
                CalloutType.Warning => "console.warnicon",
                CalloutType.Error => "console.erroricon",
                _ => "console.infoicon"
            });

            callout.EnableInClassList("evo-callout--success", type == CalloutType.Success);
            callout.EnableInClassList("evo-callout--warning", type == CalloutType.Warning);
            callout.EnableInClassList("evo-callout--error", type == CalloutType.Error);
        }
        #endregion

        #region Segments
        /// <summary>
        /// Options side by side on a well, with the chosen one raised. Clicking another option calls onChanged.
        /// </summary>
        public class SegmentedControl<T> : VisualElement
        {
            readonly List<(T option, Button button)> segments = new();
            readonly Action<T> onChanged;
            T current;

            public SegmentedControl(Action<T> onChanged)
            {
                this.onChanged = onChanged;
                AddToClassList("evo-well");
                AddToClassList("evo-segments");
            }

            public T Value => current;

            public SegmentedControl<T> AddOption(T option, string text, string tooltip = null)
            {
                var button = new Button(() => Select(option)) { text = text, tooltip = tooltip, focusable = false };
                button.AddToClassList("evo-segment");
                button.EnableInClassList("evo-segment--selected", Same(option, current));
                segments.Add((option, button));
                Add(button);
                return this;
            }

            public void SetValueWithoutNotify(T value)
            {
                current = value;
                foreach (var (option, button) in segments)
                    button.EnableInClassList("evo-segment--selected", Same(option, value));
            }

            public void SetText(T option, string text)
            {
                foreach (var (segmentOption, button) in segments)
                {
                    if (Same(segmentOption, option))
                        button.text = text;
                }
            }

            void Select(T option)
            {
                if (Same(option, current))
                    return;

                SetValueWithoutNotify(option);
                onChanged?.Invoke(option);
            }

            static bool Same(T left, T right) => EqualityComparer<T>.Default.Equals(left, right);
        }
        #endregion

        #region Progress
        /// <summary>
        /// Thin rounded track whose accent fill shows how far something has got.
        /// </summary>
        public class ProgressTrack : VisualElement
        {
            readonly VisualElement fill;

            public ProgressTrack()
            {
                AddToClassList("evo-progress");

                fill = new VisualElement { pickingMode = PickingMode.Ignore };
                fill.AddToClassList("evo-progress__fill");
                Add(fill);
            }

            public void SetProgress(float value) => fill.style.width = Length.Percent(Mathf.Clamp01(value) * 100);
        }

        /// <summary>
        /// The editor's spinning wait icon. Call Step from the editor loop while it's shown.
        /// </summary>
        public class Spinner : Image
        {
            const double FrameTime = 0.08;
            int frame;
            double nextFrame;

            public Spinner()
            {
                AddToClassList("evo-spinner");
                pickingMode = PickingMode.Ignore;
                image = GetIcon("WaitSpin00");
            }

            public void Step()
            {
                double now = EditorApplication.timeSinceStartup;
                if (now < nextFrame || resolvedStyle.display == DisplayStyle.None)
                    return;

                nextFrame = now + FrameTime;
                frame = (frame + 1) % 12;
                image = GetIcon($"WaitSpin{frame:00}");
            }
        }
        #endregion
    }
}
