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

namespace UnityEditor.Searcher
{
    class SearcherControl : VisualElement
    {
        // Window constants.
        const string k_WindowTitleLabel = "windowTitleLabel";
        const string k_WindowDetailsPanel = "windowDetailsVisualContainer";
        const string k_WindowResultsScrollViewName = "windowResultsScrollView";
        const string k_WindowSearchTextFieldName = "searchBox";
        const string k_WindowAutoCompleteLabelName = "autoCompleteLabel";
        const string k_WindowSearchIconName = "searchIcon";
        const string k_WindowResizerName = "windowResizer";
        const string kWindowSearcherPanel = "searcherVisualContainer";
        const int k_TabCharacter = 9;

        Label m_AutoCompleteLabel;
        IEnumerable<SearcherItem> m_Results;
        List<SearcherItem> m_VisibleResults;
        HashSet<SearcherItem> m_ExpandedResults;
        Searcher m_Searcher;
        string m_SuggestedTerm;
        string m_Text = string.Empty;
        Action<SearcherItem> m_SelectionCallback;
        Action<Searcher.AnalyticsEvent> m_AnalyticsDataCallback;
        ListView m_ListView;
        TextField m_SearchTextField;
        VisualElement m_SearchTextInput;
        VisualElement m_DetailsPanel;
        VisualElement m_SearcherPanel;

        internal Label TitleLabel { get; }
        internal VisualElement Resizer { get; }

        public SearcherControl()
        {
            // Load window template.
            var windowUxmlTemplate = Resources.Load<VisualTreeAsset>("SearcherWindow");

            // Clone Window Template.
            var windowRootVisualElement = windowUxmlTemplate.CloneTree();
            windowRootVisualElement.AddToClassList("content");

            windowRootVisualElement.StretchToParentSize();

            // Add Window VisualElement to window's RootVisualContainer
            Add(windowRootVisualElement);

            m_VisibleResults = new List<SearcherItem>();
            m_ExpandedResults = new HashSet<SearcherItem>();

            m_ListView = this.Q<ListView>(k_WindowResultsScrollViewName);


            if (m_ListView != null)
            {
                m_ListView.bindItem = Bind;
                m_ListView.RegisterCallback<KeyDownEvent>(SetSelectedElementInResultsList);

#if UNITY_2020_1_OR_NEWER
                m_ListView.onItemsChosen += obj => m_SelectionCallback((SearcherItem)obj.FirstOrDefault());
                m_ListView.onSelectionChange += selectedItems => m_Searcher.Adapter.OnSelectionChanged(selectedItems.OfType<SearcherItem>().ToList());
#else
                m_ListView.onItemChosen += obj => m_SelectionCallback((SearcherItem)obj);
                m_ListView.onSelectionChanged += selectedItems => m_Searcher.Adapter.OnSelectionChanged(selectedItems.OfType<SearcherItem>());
#endif
                m_ListView.focusable = true;
                m_ListView.tabIndex = 1;
            }

            m_DetailsPanel = this.Q(k_WindowDetailsPanel);

            TitleLabel = this.Q<Label>(k_WindowTitleLabel);

            m_SearcherPanel = this.Q(kWindowSearcherPanel);

            m_SearchTextField = this.Q<TextField>(k_WindowSearchTextFieldName);
            if (m_SearchTextField != null)
            {
                m_SearchTextField.focusable = true;
                m_SearchTextField.RegisterCallback<InputEvent>(OnSearchTextFieldTextChanged);

                m_SearchTextInput = m_SearchTextField.Q(TextInputBaseField<string>.textInputUssName);
                m_SearchTextInput.RegisterCallback<KeyDownEvent>(OnSearchTextFieldKeyDown);
            }

            m_AutoCompleteLabel = this.Q<Label>(k_WindowAutoCompleteLabelName);

            Resizer = this.Q(k_WindowResizerName);

            RegisterCallback<AttachToPanelEvent>(OnEnterPanel);
            RegisterCallback<DetachFromPanelEvent>(OnLeavePanel);

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

            style.flexGrow = 1;
        }

        void HackDueToListViewScrollViewStealingFocus()
        {
            m_SearchTextInput?.Focus();
            // ReSharper disable once DelegateSubtraction
            EditorApplication.update -= HackDueToListViewScrollViewStealingFocus;
        }

        void OnEnterPanel(AttachToPanelEvent e)
        {
            RegisterCallback<KeyDownEvent>(OnKeyDown);
        }

        void OnLeavePanel(DetachFromPanelEvent e)
        {
            UnregisterCallback<KeyDownEvent>(OnKeyDown);
        }

        void OnKeyDown(KeyDownEvent e)
        {
            if (e.keyCode == KeyCode.Escape)
            {
                CancelSearch();
            }
        }

        void CancelSearch()
        {
            OnSearchTextFieldTextChanged(InputEvent.GetPooled(m_Text, string.Empty));
            m_SelectionCallback(null);
            m_AnalyticsDataCallback?.Invoke(new Searcher.AnalyticsEvent(Searcher.AnalyticsEvent.EventType.Cancelled, m_SearchTextField.value));
        }

        public void Setup(Searcher searcher, Action<SearcherItem> selectionCallback, Action<Searcher.AnalyticsEvent> analyticsDataCallback)
        {
            m_Searcher = searcher;
            m_SelectionCallback = selectionCallback;
            m_AnalyticsDataCallback = analyticsDataCallback;

            if (m_Searcher.Adapter.HasDetailsPanel)
            {
                m_Searcher.Adapter.InitDetailsPanel(m_DetailsPanel);
                m_DetailsPanel.RemoveFromClassList("hidden");
                m_DetailsPanel.style.flexGrow = m_Searcher.Adapter.InitialSplitterDetailRatio;
                m_SearcherPanel.style.flexGrow = 1;
            }
            else
            {
                m_DetailsPanel.AddToClassList("hidden");

                var splitter = m_DetailsPanel.parent;

                splitter.parent.Insert(0,m_SearcherPanel);
                splitter.parent.Insert(1, m_DetailsPanel);

                splitter.RemoveFromHierarchy();
            }

            

            TitleLabel.text = m_Searcher.Adapter.Title;
            if (string.IsNullOrEmpty(TitleLabel.text))
            {
                TitleLabel.parent.style.visibility = Visibility.Hidden;
                TitleLabel.parent.style.position = Position.Absolute;
            }

            m_Searcher.BuildIndices();
            Refresh();
        }

        void Refresh()
        {
            var query = m_Text;
            m_Results = m_Searcher.Search(query);
            GenerateVisibleResults();

            // The first item in the results is always the highest scored item.
            // We want to scroll to and select this item.
            var visibleIndex = -1;
            m_SuggestedTerm = string.Empty;

            var results = m_Results.ToList();
            if (results.Any())
            {
                var scrollToItem = results.First();
                visibleIndex = m_VisibleResults.IndexOf(scrollToItem);

                var cursorIndex = m_SearchTextField.cursorIndex;

                if (query.Length > 0)
                {
                    var strings = scrollToItem.Name.Split(' ');
                    var wordStartIndex = cursorIndex == 0 ? 0 : query.LastIndexOf(' ', cursorIndex - 1) + 1;
                    var word = query.Substring(wordStartIndex, cursorIndex - wordStartIndex);

                    if (word.Length > 0)
                        foreach (var t in strings)
                        {
                            if (t.StartsWith(word, StringComparison.OrdinalIgnoreCase))
                            {
                                m_SuggestedTerm = t;
                                break;
                            }
                        }
                }
            }

            m_ListView.itemsSource = m_VisibleResults;
            m_ListView.makeItem = m_Searcher.Adapter.MakeItem;
            m_ListView.Refresh();

            SetSelectedElementInResultsList(visibleIndex);
        }

        void GenerateVisibleResults()
        {
            if (string.IsNullOrEmpty(m_Text))
            {
                m_ExpandedResults.Clear();
                RemoveChildrenFromResults();
                return;
            }

            RegenerateVisibleResults();
            ExpandAllParents();
        }

        void ExpandAllParents()
        {
            m_ExpandedResults.Clear();
            foreach (var item in m_VisibleResults)
                if (item.HasChildren)
                    m_ExpandedResults.Add(item);
        }

        void RemoveChildrenFromResults()
        {
            m_VisibleResults.Clear();
            var parents = new HashSet<SearcherItem>();

            foreach (var item in m_Results.Where(i => !parents.Contains(i)))
            {
                var currentParent = item;

                while (true)
                {
                    if (currentParent.Parent == null)
                    {
                        if (parents.Contains(currentParent))
                            break;

                        parents.Add(currentParent);
                        m_VisibleResults.Add(currentParent);
                        break;
                    }

                    currentParent = currentParent.Parent;
                }
            }

            if (m_Searcher.SortComparison != null)
                m_VisibleResults.Sort(m_Searcher.SortComparison);
        }

        void RegenerateVisibleResults()
        {
            var idSet = new HashSet<SearcherItem>();
            m_VisibleResults.Clear();

            foreach (var item in m_Results.Where(item => !idSet.Contains(item)))
            {
                idSet.Add(item);
                m_VisibleResults.Add(item);

                var currentParent = item.Parent;
                while (currentParent != null)
                {
                    if (!idSet.Contains(currentParent))
                    {
                        idSet.Add(currentParent);
                        m_VisibleResults.Add(currentParent);
                    }

                    currentParent = currentParent.Parent;
                }

                AddResultChildren(item, idSet);
            }

            var comparison = m_Searcher.SortComparison ?? ((i1, i2) =>
            {
                var result = i1.Database.Id - i2.Database.Id;
                return result != 0 ? result : i1.Id - i2.Id;
            });
            m_VisibleResults.Sort(comparison);
        }

        void AddResultChildren(SearcherItem item, ISet<SearcherItem> idSet)
        {
            if (!item.HasChildren)
                return;
            if (m_Searcher.Adapter.AddAllChildResults)
            {
                //add all children results for current search term 
                // eg "Book" will show both "Cook Book" and "Cooking" as children
                foreach (var child in item.Children)
                {
                    if (!idSet.Contains(child))
                    {
                        idSet.Add(child);
                        m_VisibleResults.Add(child);
                    }

                    AddResultChildren(child, idSet);
                }
            }
            else
            {
                foreach (var child in item.Children)
                {
                    //only add child results if the child matches the search term 
                    // eg "Book" will show "Cook Book" but not "Cooking" as a child
                    if (!m_Results.Contains(child))
                        continue;

                    if (!idSet.Contains(child))
                    {
                        idSet.Add(child);
                        m_VisibleResults.Add(child);
                    }

                    AddResultChildren(child, idSet);
                }
            } 
        }

        bool HasChildResult(SearcherItem item)
        {
            if (m_Results.Contains(item))
                return true;

            foreach (var child in item.Children)
            {
                if (HasChildResult(child))
                    return true;
            }

            return false;
        }

        ItemExpanderState GetExpanderState(int index)
        {
            var item = m_VisibleResults[index];

            foreach (var child in item.Children)
            {
                if (!m_VisibleResults.Contains(child) && !HasChildResult(child))
                    continue;

                return m_ExpandedResults.Contains(item) ? ItemExpanderState.Expanded : ItemExpanderState.Collapsed;
            }

            return ItemExpanderState.Hidden;
        }

        void Bind(VisualElement target, int index)
        {
            var item = m_VisibleResults[index];
            var expanderState = GetExpanderState(index);
            var expander = m_Searcher.Adapter.Bind(target, item, expanderState, m_Text);
            expander.RegisterCallback<MouseDownEvent>(ExpandOrCollapse);
        }

        static void GetItemsToHide(SearcherItem parent, ref HashSet<SearcherItem> itemsToHide)
        {
            if (!parent.HasChildren)
            {
                itemsToHide.Add(parent);
                return;
            }

            foreach (var child in parent.Children)
            {
                itemsToHide.Add(child);
                GetItemsToHide(child, ref itemsToHide);
            }
        }

        void HideUnexpandedItems()
        {
            // Hide unexpanded children.
            var itemsToHide = new HashSet<SearcherItem>();
            foreach (var item in m_VisibleResults)
            {
                if (m_ExpandedResults.Contains(item))
                    continue;

                if (!item.HasChildren)
                    continue;

                if (itemsToHide.Contains(item))
                    continue;

                // We need to hide its children.
                GetItemsToHide(item, ref itemsToHide);
            }

            foreach (var item in itemsToHide)
                m_VisibleResults.Remove(item);
        }

        // ReSharper disable once UnusedMember.Local
        void RefreshListViewOn()
        {
            // TODO: Call ListView.Refresh() when it is fixed.
            // Need this workaround until then.
            // See: https://fogbugz.unity3d.com/f/cases/1027728/
            // And: https://gitlab.internal.unity3d.com/upm-packages/editor/com.unity.searcher/issues/9

            var scrollView = m_ListView.Q<ScrollView>();

            var scroller = scrollView?.Q<Scroller>("VerticalScroller");
            if (scroller == null)
                return;

            var oldValue = scroller.value;
            scroller.value = oldValue + 1.0f;
            scroller.value = oldValue - 1.0f;
            scroller.value = oldValue;
        }

        void Expand(SearcherItem item)
        {
            m_ExpandedResults.Add(item);

            RegenerateVisibleResults();
            HideUnexpandedItems();

            m_ListView.Refresh();
        }

        void Collapse(SearcherItem item)
        {
            // if it's already collapsed or not collapsed
            if (!m_ExpandedResults.Remove(item))
            {
                // this case applies for a left arrow key press
                if (item.Parent != null)
                    SetSelectedElementInResultsList(m_VisibleResults.IndexOf(item.Parent));

                // even if it's a root item and has no parents, do nothing more
                return;
            }

            RegenerateVisibleResults();
            HideUnexpandedItems();

            // TODO: understand what happened
            m_ListView.Refresh();

            // RefreshListViewOn();
        }

        void ExpandOrCollapse(MouseDownEvent evt)
        {
            if (!(evt.target is VisualElement expanderLabel))
                return;

            VisualElement itemElement = expanderLabel.GetFirstAncestorOfType<TemplateContainer>();

            if (!(itemElement?.userData is SearcherItem item)
                || !item.HasChildren
                || !expanderLabel.ClassListContains("Expanded") && !expanderLabel.ClassListContains("Collapsed"))
                return;

            if (!m_ExpandedResults.Contains(item))
                Expand(item);
            else
                Collapse(item);

            evt.StopImmediatePropagation();
        }

        void OnSearchTextFieldTextChanged(InputEvent inputEvent)
        {
            var text = inputEvent.newData;

            if (string.Equals(text, m_Text))
                return;

            // This is necessary due to OnTextChanged(...) being called after user inputs that have no impact on the text.
            // Ex: Moving the caret.
            m_Text = text;

            // If backspace is pressed and no text remain, clear the suggestion label.
            if (string.IsNullOrEmpty(text))
            {
                this.Q(k_WindowSearchIconName).RemoveFromClassList("Active");

                // Display the unfiltered results list.
                Refresh();

                m_AutoCompleteLabel.text = String.Empty;
                m_SuggestedTerm = String.Empty;

                SetSelectedElementInResultsList(0);

                return;
            }

            if (!this.Q(k_WindowSearchIconName).ClassListContains("Active"))
                this.Q(k_WindowSearchIconName).AddToClassList("Active");

            Refresh();

            // Calculate the start and end indexes of the word being modified (if any).
            var cursorIndex = m_SearchTextField.cursorIndex;

            // search toward the beginning of the string starting at the character before the cursor
            // +1 because we want the char after a space, or 0 if the search fails
            var wordStartIndex = cursorIndex == 0 ? 0 : (text.LastIndexOf(' ', cursorIndex - 1) + 1);

            // search toward the end of the string from the cursor index
            var wordEndIndex = text.IndexOf(' ', cursorIndex);
            if (wordEndIndex == -1) // no space found, assume end of string
                wordEndIndex = text.Length;

            // Clear the suggestion term if the caret is not within a word (both start and end indexes are equal, ex: (space)caret(space))
            // or the user didn't append characters to a word at the end of the query.
            if (wordStartIndex == wordEndIndex || wordEndIndex < text.Length)
            {
                m_AutoCompleteLabel.text = string.Empty;
                m_SuggestedTerm = string.Empty;
                return;
            }

            var word = text.Substring(wordStartIndex, wordEndIndex - wordStartIndex);

            if (!string.IsNullOrEmpty(m_SuggestedTerm))
            {
                var wordSuggestion =
                    word + m_SuggestedTerm.Substring(word.Length, m_SuggestedTerm.Length - word.Length);
                text = text.Remove(wordStartIndex, word.Length);
                text = text.Insert(wordStartIndex, wordSuggestion);
                m_AutoCompleteLabel.text = text;
            }
            else
            {
                m_AutoCompleteLabel.text = String.Empty;
            }
        }

        void OnSearchTextFieldKeyDown(KeyDownEvent keyDownEvent)
        {
            // First, check if we cancelled the search.
            if (keyDownEvent.keyCode == KeyCode.Escape)
            {
                CancelSearch();
                return;
            }

            // For some reason the KeyDown event is raised twice when entering a character.
            // As such, we ignore one of the duplicate event.
            // This workaround was recommended by the Editor team. The cause of the issue relates to how IMGUI works
            // and a fix was not in the works at the moment of this writing.
            if (keyDownEvent.character == k_TabCharacter)
            {
                // Prevent switching focus to another visual element.
                keyDownEvent.PreventDefault();

                return;
            }

            // If Tab is pressed, complete the query with the suggested term.
            if (keyDownEvent.keyCode == KeyCode.Tab)
            {
                // Used to prevent the TAB input from executing it's default behavior. We're hijacking it for auto-completion.
                keyDownEvent.PreventDefault();

                if (!string.IsNullOrEmpty(m_SuggestedTerm))
                {
                    SelectAndReplaceCurrentWord();
                    m_AutoCompleteLabel.text = string.Empty;

                    // TODO: Revisit, we shouldn't need to do this here.
                    m_Text = m_SearchTextField.text;

                    Refresh();

                    m_SuggestedTerm = string.Empty;
                }
            }
            else
            {
                SetSelectedElementInResultsList(keyDownEvent);
            }
        }

        void SelectAndReplaceCurrentWord()
        {
            var s = m_SearchTextField.value;
            var lastWordIndex = s.LastIndexOf(' ');
            lastWordIndex++;

            var newText = s.Substring(0, lastWordIndex) + m_SuggestedTerm;

            // Wait for SelectRange api to reach trunk
//#if UNITY_2018_3_OR_NEWER
//            m_SearchTextField.value = newText;
//            m_SearchTextField.SelectRange(m_SearchTextField.value.Length, m_SearchTextField.value.Length);
//#else
            // HACK - relies on the textfield moving the caret when being assigned a value and skipping
            // all low surrogate characters
            var magicMoveCursorToEndString = new string('\uDC00', newText.Length);
            m_SearchTextField.value = magicMoveCursorToEndString;
            m_SearchTextField.value = newText;

//#endif
        }

        void SetSelectedElementInResultsList(KeyDownEvent keyDownEvent)
        {
            int index;
            switch (keyDownEvent.keyCode)
            {
                case KeyCode.Escape:
                    m_SelectionCallback(null);
                    m_AnalyticsDataCallback?.Invoke(new Searcher.AnalyticsEvent(Searcher.AnalyticsEvent.EventType.Cancelled, m_SearchTextField.value));
                    break;
                case KeyCode.Return:
                case KeyCode.KeypadEnter:
                    if (m_ListView.selectedIndex != -1)
                    {
                        m_SelectionCallback((SearcherItem)m_ListView.selectedItem);
                        m_AnalyticsDataCallback?.Invoke(new Searcher.AnalyticsEvent(Searcher.AnalyticsEvent.EventType.Picked, m_SearchTextField.value));
                    }
                    else
                    {
                        m_SelectionCallback(null);
                        m_AnalyticsDataCallback?.Invoke(new Searcher.AnalyticsEvent(Searcher.AnalyticsEvent.EventType.Cancelled, m_SearchTextField.value));
                    }
                    break;
                case KeyCode.LeftArrow:
                    index = m_ListView.selectedIndex;
                    if (index >= 0 && index < m_ListView.itemsSource.Count)
                        Collapse(m_ListView.selectedItem as SearcherItem);
                    break;
                case KeyCode.RightArrow:
                    index = m_ListView.selectedIndex;
                    if (index >= 0 && index < m_ListView.itemsSource.Count)
                        Expand(m_ListView.selectedItem as SearcherItem);
                    break;
                case KeyCode.UpArrow:
                case KeyCode.DownArrow:
                case KeyCode.PageUp:
                case KeyCode.PageDown:
                    index = m_ListView.selectedIndex;
                    if (index >= 0 && index < m_ListView.itemsSource.Count)
                        m_ListView.OnKeyDown(keyDownEvent);
                    break;
            }
        }

        void SetSelectedElementInResultsList(int selectedIndex)
        {
            var newIndex = selectedIndex >= 0 && selectedIndex < m_VisibleResults.Count ? selectedIndex : -1;
            if (newIndex < 0)
                return;

            m_ListView.selectedIndex = newIndex;
            m_ListView.ScrollToItem(m_ListView.selectedIndex);
        }
    }
}
