using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using UnityEngine;
using UnityEngine.UIElements;

namespace UnityEditor.Searcher
{
    [PublicAPI]
    public class SearcherWindow : EditorWindow
    {
        [PublicAPI]
        public struct Alignment
        {
            [PublicAPI]
            public enum Horizontal { Left = 0, Center, Right }
            [PublicAPI]
            public enum Vertical { Top = 0, Center, Bottom }

            public readonly Vertical vertical;
            public readonly Horizontal horizontal;

            public Alignment(Vertical v, Horizontal h)
            {
                vertical = v;
                horizontal = h;
            }
        }

        const string k_DatabaseDirectory = "/../Library/Searcher";

        static readonly float k_SearcherDefaultWidth = 300;
        static readonly float k_DetailsDefaultWidth = 200;
        static readonly float k_DefaultHeight = 300;
        static readonly Vector2 k_MinSize = new Vector2(300, 150);

        static Vector2 s_Size = Vector2.zero;
        static IEnumerable<SearcherItem> s_Items;
        static Searcher s_Searcher;
        static Func<SearcherItem, bool> s_ItemSelectedDelegate;

        Action<Searcher.AnalyticsEvent> m_AnalyticsDataDelegate;
        SearcherControl m_SearcherControl;
        Vector2 m_OriginalMousePos;
        Rect m_OriginalWindowPos;
        Rect m_NewWindowPos;
        bool m_IsMouseDownOnResizer;
        bool m_IsMouseDownOnTitle;
        Focusable m_FocusedBefore;

        static Vector2 Size
        {
            get
            {
                if (s_Size == Vector2.zero)
                {
                    s_Size = s_Searcher != null && s_Searcher.Adapter.HasDetailsPanel
                        ? new Vector2(k_SearcherDefaultWidth + k_DetailsDefaultWidth, k_DefaultHeight)
                        : new Vector2(k_SearcherDefaultWidth, k_DefaultHeight);
                }

                return s_Size;
            }
            set => s_Size = value;
        }

        public static void Show(
            EditorWindow host,
            IList<SearcherItem> items,
            string title,
            Func<SearcherItem, bool> itemSelectedDelegate,
            Vector2 displayPosition,
            Alignment align = default)
        {
            Show(host, items, title, Application.dataPath + k_DatabaseDirectory, itemSelectedDelegate, displayPosition, align);
        }

        public static void Show(
            EditorWindow host,
            IList<SearcherItem> items,
            ISearcherAdapter adapter,
            Func<SearcherItem, bool> itemSelectedDelegate,
            Vector2 displayPosition,
            Action<Searcher.AnalyticsEvent> analyticsDataDelegate,
            Alignment align = default)
        {
            Show(host, items, adapter, Application.dataPath + k_DatabaseDirectory, itemSelectedDelegate,
                displayPosition, analyticsDataDelegate, align);
        }

        public static void Show(
            EditorWindow host,
            IList<SearcherItem> items,
            string title,
            string directoryPath,
            Func<SearcherItem, bool> itemSelectedDelegate,
            Vector2 displayPosition,
            Alignment align = default)
        {
            s_Items = items;
            var databaseDir = directoryPath;
            var database = SearcherDatabase.Create(s_Items.ToList(), databaseDir);
            s_Searcher = new Searcher(database, title);

            Show(host, s_Searcher, itemSelectedDelegate, displayPosition, null, align);
        }

        public static void Show(
            EditorWindow host,
            IEnumerable<SearcherItem> items,
            ISearcherAdapter adapter,
            string directoryPath,
            Func<SearcherItem, bool> itemSelectedDelegate,
            Vector2 displayPosition,
            Action<Searcher.AnalyticsEvent> analyticsDataDelegate,
            Alignment align = default)
        {
            s_Items = items;
            var databaseDir = directoryPath;
            var database = SearcherDatabase.Create(s_Items.ToList(), databaseDir);
            s_Searcher = new Searcher(database, adapter);

            Show(host, s_Searcher, itemSelectedDelegate, displayPosition, analyticsDataDelegate, align);
        }

        public static void Show(
            EditorWindow host,
            Searcher searcher,
            Func<SearcherItem, bool> itemSelectedDelegate,
            Vector2 displayPosition,
            Action<Searcher.AnalyticsEvent> analyticsDataDelegate,
            Alignment align = default)
        {
            var position = GetPosition(host, displayPosition, align);
            var rect = new Rect(GetPositionWithAlignment(position + host.position.position, Size, align), Size);

            Show(host, searcher, itemSelectedDelegate, analyticsDataDelegate, rect);
        }
        public static void Show(
            EditorWindow host,
            Searcher searcher,
            Func<SearcherItem, bool> itemSelectedDelegate,
            Action<Searcher.AnalyticsEvent> analyticsDataDelegate,
            Rect rect)
        {
            s_Searcher = searcher;
            s_ItemSelectedDelegate = itemSelectedDelegate;

            var window = CreateInstance<SearcherWindow>();
            window.m_AnalyticsDataDelegate = analyticsDataDelegate;
            window.position = rect;
            window.ShowPopup();
            window.Focus();
        }

        public static Vector2 GetPositionWithAlignment(Vector2 pos, Vector2 size, Alignment align)
        {
            var x = pos.x;
            var y = pos.y;

            switch (align.horizontal)
            {
                case Alignment.Horizontal.Center:
                    x -= size.x / 2;
                    break;

                case Alignment.Horizontal.Right:
                    x -= size.x;
                    break;
            }

            switch (align.vertical)
            {
                case Alignment.Vertical.Center:
                    y -= size.y / 2;
                    break;

                case Alignment.Vertical.Bottom:
                    y -= size.y;
                    break;
            }

            return new Vector2(x, y);
        }

        static Vector2 GetPosition(EditorWindow host, Vector2 displayPosition, Alignment align)
        {
            var x = displayPosition.x;
            var y = displayPosition.y;

            // Searcher overlaps with the right boundary.
            if (x + Size.x >= host.position.size.x)
            {
                switch (align.horizontal)
                {
                    case Alignment.Horizontal.Center:
                        x -= Size.x / 2;
                        break;

                    case Alignment.Horizontal.Right:
                        x -= Size.x;
                        break;
                }
            }

            // The displayPosition should be in window world space but the
            // EditorWindow.position is actually the rootVisualElement
            // rectangle, not including the tabs area. So we need to do a
            // small correction here.
            y -= host.rootVisualElement.resolvedStyle.top;

            // Searcher overlaps with the bottom boundary.
            if (y + Size.y >= host.position.size.y)
            {
                switch (align.vertical)
                {
                    case Alignment.Vertical.Center:
                        y -= Size.y / 2;
                        break;

                    case Alignment.Vertical.Bottom:
                        y -= Size.y;
                        break;
                }
            }

            return new Vector2(x, y);
        }

        void OnEnable()
        {
            m_SearcherControl = new SearcherControl();
            m_SearcherControl.Setup(s_Searcher, SelectionCallback, OnAnalyticsDataCallback, s_Searcher.Adapter.OnSearchResultsFilter);

            m_SearcherControl.TitleLabel.RegisterCallback<MouseDownEvent>(OnTitleMouseDown);
            m_SearcherControl.TitleLabel.RegisterCallback<MouseUpEvent>(OnTitleMouseUp);

            m_SearcherControl.Resizer.RegisterCallback<MouseDownEvent>(OnResizerMouseDown);
            m_SearcherControl.Resizer.RegisterCallback<MouseUpEvent>(OnResizerMouseUp);

            var root = rootVisualElement;
            root.style.flexGrow = 1;
            root.Add(m_SearcherControl);
        }

        void OnDisable()
        {
            m_SearcherControl.TitleLabel.UnregisterCallback<MouseDownEvent>(OnTitleMouseDown);
            m_SearcherControl.TitleLabel.UnregisterCallback<MouseUpEvent>(OnTitleMouseUp);

            m_SearcherControl.Resizer.UnregisterCallback<MouseDownEvent>(OnResizerMouseDown);
            m_SearcherControl.Resizer.UnregisterCallback<MouseUpEvent>(OnResizerMouseUp);
        }

        void OnTitleMouseDown(MouseDownEvent evt)
        {
            if (evt.button != (int)MouseButton.LeftMouse)
                return;

            m_IsMouseDownOnTitle = true;

            m_NewWindowPos = position;
            m_OriginalWindowPos = position;
            m_OriginalMousePos = evt.mousePosition;

            m_FocusedBefore = rootVisualElement.panel.focusController.focusedElement;

            m_SearcherControl.TitleLabel.RegisterCallback<MouseMoveEvent>(OnTitleMouseMove);
            m_SearcherControl.TitleLabel.RegisterCallback<KeyDownEvent>(OnSearcherKeyDown);
            m_SearcherControl.TitleLabel.CaptureMouse();
        }

        void OnTitleMouseUp(MouseUpEvent evt)
        {
            if (evt.button != (int)MouseButton.LeftMouse)
                return;

            if (!m_SearcherControl.TitleLabel.HasMouseCapture())
                return;

            FinishMove();
        }

        void FinishMove()
        {
            m_SearcherControl.TitleLabel.UnregisterCallback<MouseMoveEvent>(OnTitleMouseMove);
            m_SearcherControl.TitleLabel.UnregisterCallback<KeyDownEvent>(OnSearcherKeyDown);
            m_SearcherControl.TitleLabel.ReleaseMouse();
            m_FocusedBefore?.Focus();
            m_IsMouseDownOnTitle = false;
        }

        void OnTitleMouseMove(MouseMoveEvent evt)
        {
            var delta = evt.mousePosition - m_OriginalMousePos;

            // TODO Temporary fix for Visual Scripting 1st drop. Find why position.position is 0,0 on MacOs in MouseMoveEvent
            // Bug occurs with Unity 2019.2.0a13
#if UNITY_EDITOR_OSX
            m_NewWindowPos = new Rect(m_NewWindowPos.position + delta, position.size);
#else
            m_NewWindowPos = new Rect(position.position + delta, position.size);
#endif
            Repaint();
        }

        void OnResizerMouseDown(MouseDownEvent evt)
        {
            if (evt.button != (int)MouseButton.LeftMouse)
                return;

            m_IsMouseDownOnResizer = true;

            m_NewWindowPos = position;
            m_OriginalWindowPos = position;
            m_OriginalMousePos = evt.mousePosition;

            m_FocusedBefore = rootVisualElement.panel.focusController.focusedElement;

            m_SearcherControl.Resizer.RegisterCallback<MouseMoveEvent>(OnResizerMouseMove);
            m_SearcherControl.Resizer.RegisterCallback<KeyDownEvent>(OnSearcherKeyDown);
            m_SearcherControl.Resizer.CaptureMouse();
        }

        void OnResizerMouseUp(MouseUpEvent evt)
        {
            if (evt.button != (int)MouseButton.LeftMouse)
                return;

            if (!m_SearcherControl.Resizer.HasMouseCapture())
                return;

            FinishResize();
        }

        void FinishResize()
        {
            m_SearcherControl.Resizer.UnregisterCallback<MouseMoveEvent>(OnResizerMouseMove);
            m_SearcherControl.Resizer.UnregisterCallback<KeyDownEvent>(OnSearcherKeyDown);
            m_SearcherControl.Resizer.ReleaseMouse();
            m_FocusedBefore?.Focus();
            m_IsMouseDownOnResizer = false;
        }

        void OnResizerMouseMove(MouseMoveEvent evt)
        {
            var delta = evt.mousePosition - m_OriginalMousePos;
            Size = m_OriginalWindowPos.size + delta;
            Size = new Vector2(Math.Max(k_MinSize.x, Size.x), Math.Max(k_MinSize.y, Size.y));

            // TODO Temporary fix for Visual Scripting 1st drop. Find why position.position is 0,0 on MacOs in MouseMoveEvent
            // Bug occurs with Unity 2019.2.0a13
#if UNITY_EDITOR_OSX
            m_NewWindowPos = new Rect(m_NewWindowPos.position, Size);
#else
            m_NewWindowPos = new Rect(position.position, Size);
#endif
            Repaint();
        }

        void OnSearcherKeyDown(KeyDownEvent evt)
        {
            if (evt.keyCode == KeyCode.Escape)
            {
                if (m_IsMouseDownOnTitle)
                {
                    FinishMove();
                    position = m_OriginalWindowPos;
                }
                else if (m_IsMouseDownOnResizer)
                {
                    FinishResize();
                    position = m_OriginalWindowPos;
                }
            }
        }

        void OnGUI()
        {
            if ((m_IsMouseDownOnTitle || m_IsMouseDownOnResizer) && Event.current.type == EventType.Layout)
                position = m_NewWindowPos;
        }

        void SelectionCallback(SearcherItem item)
        {
            // Don't close the window if a category is selected (only categories/titles have children, node entries are leaf elements)
            // We want to prevent collapsing the window due to accidental double-clicks on a title entry, for instance
            if (item != null && item.HasChildren)
                return;

            if (s_ItemSelectedDelegate == null || s_ItemSelectedDelegate(item))
                Close();
        }

        void OnAnalyticsDataCallback(Searcher.AnalyticsEvent item)
        {
            m_AnalyticsDataDelegate?.Invoke(item);
        }

        void OnLostFocus()
        {
            if (m_IsMouseDownOnTitle)
            {
                FinishMove();
            }
            else if (m_IsMouseDownOnResizer)
            {
                FinishResize();
            }

            // TODO: HACK - ListView's scroll view steals focus using the scheduler.
            EditorApplication.update += HackDueToCloseOnLostFocusCrashing;
        }

        // See: https://fogbugz.unity3d.com/f/cases/1004504/
        void HackDueToCloseOnLostFocusCrashing()
        {
            // Notify user that the searcher action was cancelled.
            s_ItemSelectedDelegate?.Invoke(null);

            Close();

            // ReSharper disable once DelegateSubtraction
            EditorApplication.update -= HackDueToCloseOnLostFocusCrashing;
        }
    }
}
