
// using Eden.CommonEditor.UnityExtensions;
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.InputSystem;

public enum SearchResult
{
	Validate,
	Cancel,
}

public class SearchableListWindow : EditorWindow
{
	#region Delegates
	public delegate void SingleSelectionDelegate(SearchResult result, object selectedObject);
	public delegate void MultiSelectionDelegate(SearchResult result, object[] selectedObjects);
	#endregion

	#region Constants
	private static readonly string SearchBarControlName = "searchBar";
	private static readonly int MaxDisplayedUnselectedResults = 100;
	#endregion

	#region Fields
	private static GUIStyle ToolbarStyle = null;
	private static GUIStyle CancelStyle = null;
	private static GUIStyle ResultLineStyle = null;
	private static GUIStyle SelectedResultLineStyle = null;

	private bool _justShown = true;
	private bool _forceDisplayAllResults = false;
	private string _label = "Search";
	private Dictionary<object, string> _searchedEntries = null;
	private List<object> _resultsList = new List<object>();
	private string _currentCaption = "";
	private Vector2 _scrollPos;
	private HashSet<object> _selectedResults = new HashSet<object>();
	private bool _handleMultiSelection = false;
	private bool _alphabeticalSort = false;
	private object _currentSelection = null;
	#endregion

	#region Events
	private SingleSelectionDelegate _closedCallbackSingle = null;
	private MultiSelectionDelegate _closedCallbackMulti = null;
	#endregion

	#region Methods
	#region Static methods
	/// <summary>
	/// Will prompt a SearchableListWindow. User will be able to select ONE entry in the list.
	/// </summary>
	/// <param name="searchedEntries">Dictionary containing all searchable entries</param>
	/// <param name="label">Label displayed in front of the search field</param>
	/// <param name="closedCallback">Delegate that will be called once the window exits</param>
	/// <param name="defaultCaption">Default input in the search field</param>
	/// <param name="width">Specify a width if required</param>
	/// <param name="alphabeticalSort">Toggle alphabetical sort of the entries</param>
	/// <returns>Handle on the window instance.</returns>
	public static SearchableListWindow ShowSingle(IDictionary<object, string> searchedEntries, string label,
		SingleSelectionDelegate closedCallback = null, string defaultCaption = "", int width = 500, bool alphabeticalSort = false)
	{
		SearchableListWindow window = InstantiateWindow(searchedEntries, label, defaultCaption, width, alphabeticalSort, false);

		if (closedCallback != null)
		{
			window._closedCallbackSingle += closedCallback;
		}

		return window;
	}

	/// <summary>
	/// Will prompt a SearchableListWindow. User will be able to select multiple entries in the list.
	/// </summary>
	/// <param name="searchedEntries">Dictionary containing all searchable entries</param>
	/// <param name="label">Label displayed in front of the search field</param>
	/// <param name="closedCallback">Delegate that will be called once the window exits</param>
	/// <param name="defaultCaption">Default input in the search field</param>
	/// <param name="width">Specify a width if required</param>
	/// <param name="alphabeticalSort">Toggle alphabetical sort of the entries</param>
	/// <returns>Handle on the window instance.</returns>
	public static SearchableListWindow ShowMulti(Dictionary<object, string> searchedEntries, string label,
		MultiSelectionDelegate closedCallback = null, string defaultCaption = "", int width = 500, bool alphabeticalSort = false)
	{
		SearchableListWindow window = InstantiateWindow(searchedEntries, label, defaultCaption, width, alphabeticalSort, true);

		if (closedCallback != null)
		{
			window._closedCallbackMulti += closedCallback;
		}

		return window;
	}

	private static SearchableListWindow InstantiateWindow(IDictionary<object, string> searchedEntries, string label, string defaultCaption, int width, bool alphabeticalSort, bool handleMultiSelection)
	{
		// Create an instance of the file creator window
		SearchableListWindow window = (SearchableListWindow)ScriptableObject.CreateInstance("SearchableListWindow");

		window._searchedEntries = new Dictionary<object, string>(searchedEntries);
		window._label = label;
		window._currentCaption = defaultCaption;
		window._handleMultiSelection = handleMultiSelection;
		window._alphabeticalSort = alphabeticalSort;

#if UNITY_EDITOR_WIN
		// Window is created on cursor position
		POINT p;
		GetCursorPos(out p);
		window.ShowAsDropDown(new Rect(p.X - (width * 0.75f), p.Y, width, 500), new Vector2(width, 500));
#else
		window.ShowAsDropDown(new Rect(Screen.width / 2, Screen.height / 2, 0, 0), new Vector2(width, 60));
#endif

		return window;
	}

	private static void TryInitStyle()
	{
		if (ToolbarStyle == null)
		{
			ToolbarStyle = GUI.skin.FindStyle("ToolbarSeachTextField");
		}

		if (CancelStyle == null)
		{
			CancelStyle = GUI.skin.FindStyle("ToolbarSeachCancelButton");
		}

		if (ResultLineStyle == null)
		{
			ResultLineStyle = new GUIStyle(GUI.skin.FindStyle("ObjectPickerResultsEven"));
		}

		if (SelectedResultLineStyle == null)
		{
			SelectedResultLineStyle = new GUIStyle(GUI.skin.FindStyle("ObjectPickerResultsEven"));
			SelectedResultLineStyle.normal.background = SelectedResultLineStyle.active.background;
			SelectedResultLineStyle.normal.textColor = SelectedResultLineStyle.active.textColor;
		}
	}
	#endregion

	private void OnGUI()
	{
		bool refreshResults = false;

		if (_justShown)
		{
			TryInitStyle();
			refreshResults = true;
		}

		GUILayout.BeginVertical("", GUI.skin.window);

		DrawHelper();

		refreshResults |= DrawSearchBar();

		if (refreshResults)
		{
			ResfreshResults();
		}

		DrawResultsList();

		HandleNavigation(refreshResults);

		DrawFooter();

		_justShown = false;

		GUILayout.EndVertical();
	}

	private bool DrawSearchBar()
	{
		EditorGUI.BeginChangeCheck();

		EditorGUILayout.BeginHorizontal();
		{
			// Draw the search bar.
			EditorGUILayout.LabelField(_label, GUILayout.ExpandWidth(true));
			GUI.SetNextControlName(SearchBarControlName);

			_currentCaption = EditorGUILayout.TextField(_currentCaption, ToolbarStyle, GUILayout.Width(Screen.width * 0.4f)).ToUpper();

			if (_justShown)
			{
				EditorGUI.FocusTextInControl(SearchBarControlName);
			}

			// Draw the cross-shaped cancel button.
			if (GUILayout.Button("", CancelStyle))
			{
				_currentCaption = "";
				Close(SearchResult.Cancel);
			}
		}
		EditorGUILayout.EndHorizontal();

		return EditorGUI.EndChangeCheck();
	}

	private void DrawHelper()
	{
		if (_handleMultiSelection)
		{
			EditorGUILayout.HelpBox("Multi Selection : Hold Ctrl or Shift while selecting, validate with Enter.", MessageType.Info, true);
		}
		else
		{
			EditorGUILayout.HelpBox("Single Selection : click a line or navigate with arrows then validate with Enter.", MessageType.Info, true);
		}
	}

	private void ResfreshResults()
	{
		_resultsList.Clear();

		// In any case let currently selected objects in the list
		_resultsList.AddRange(_selectedResults);

		// Process each word of the search input separately.
		string[] splits = _currentCaption.Split(' ');
		foreach (KeyValuePair<object, string> searchedEntry in _searchedEntries)
		{
			bool add = true;

			// Get value, removing all white spaces
			string id = string.Join("", searchedEntry.Value.ToUpper().Split(default(string[]), StringSplitOptions.RemoveEmptyEntries));

			// To be added in the list, the id must contain all strings
			// from the Input.
			for (int splitIndex = 0; splitIndex < splits.Length; splitIndex++)
			{
				if (id.Contains(splits[splitIndex]) == false)
				{
					add = false;
					break;
				}
			}

			if (add && _resultsList.Contains(searchedEntry.Key) == false)
			{
				_resultsList.Add(searchedEntry.Key);
			}
		}

		if (_alphabeticalSort)
		{
			_resultsList.Sort((resultA, resultB) =>
			{
				return _searchedEntries[resultA].CompareTo(_searchedEntries[resultB]);
			});
		}
	}

	private void DrawResultsList()
	{
		if (_resultsList.Count > 0)
		{
			_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos, false, false);
			GUILayout.BeginVertical("", GUI.skin.box);
			object result;

			int maxResults = _forceDisplayAllResults ? int.MaxValue : _selectedResults.Count + MaxDisplayedUnselectedResults;

			for (int resultIndex = 0; resultIndex < Math.Min(_resultsList.Count, maxResults); resultIndex++)
			{
				result = _resultsList[resultIndex];

				GUIStyle style = _selectedResults.Contains(result) ? SelectedResultLineStyle : ResultLineStyle;

				if (_currentSelection == result)
				{
					GUI.contentColor = Color.cyan;
				}

				bool lineClicked = GUILayout.Button(_searchedEntries[result], style, GUILayout.ExpandWidth(true));

				GUI.contentColor = Color.white;

				if (lineClicked)
				{
					if (_handleMultiSelection == false)
					{
						// No multi-selction : just close straight away
						_selectedResults.Add(result);
						Close(SearchResult.Validate);
					}
					else
					{
						HashSet<object> newlySelectedObjects = new HashSet<object>();

						// Handle it based on input modifier
						if (Keyboard.current.leftShiftKey.isPressed)
						{
							// Shift means multi select from current selection
							int currentIndex = _resultsList.IndexOf(_currentSelection);

							for (int newlySelectedObjectIndex = Math.Min(currentIndex, resultIndex); newlySelectedObjectIndex < Math.Max(currentIndex, resultIndex) + 1; newlySelectedObjectIndex++)
							{
								newlySelectedObjects.Add(_resultsList[newlySelectedObjectIndex]);
							}
						}
						else
						{
							// No shift : only that object
							newlySelectedObjects.Add(result);
						}

						// Then test ctrl
						if (Keyboard.current.leftCtrlKey.isPressed)
						{
							// Add/Remove current selection depending on selection
							if ((newlySelectedObjects.Count > 1 && _selectedResults.Contains(_currentSelection)) || _selectedResults.Contains(result) == false)
							{
								_selectedResults.UnionWith(newlySelectedObjects);
							}
							else
							{
								_selectedResults.ExceptWith(newlySelectedObjects);
							}
						}
						else
						{
							// Replace with those objects
							_selectedResults = newlySelectedObjects;
						}

						// in any case if there is only one object, that becomes the new selection
						if (newlySelectedObjects.Count == 1)
						{
							_currentSelection = result;
						}
					}
				}
			}
			if (_resultsList.Count > maxResults)
			{
				if (GUILayout.Button("... " + (_resultsList.Count - maxResults) + " more (click to show all)", ResultLineStyle))
				{
					_forceDisplayAllResults = true;
				}
			}

			GUILayout.EndVertical();
			EditorGUILayout.EndScrollView();
		}
	}

	private void HandleNavigation(bool resultsRefreshed)
	{
		Event input = Event.current;

		// Arrows Navigation
		int currentIndex = _resultsList.IndexOf(_currentSelection);
		int newIndex = currentIndex;

		if (Keyboard.current.upArrowKey.isPressed)
		{
			newIndex = Math.Max(currentIndex - 1, 0);
		}
		else if (Keyboard.current.downArrowKey.isPressed)
		{
			newIndex = Math.Min(currentIndex + 1, _resultsList.Count - 1);
		}
		else if (_resultsList.Count == 1)
		{
			newIndex = 0;
		}

		if (newIndex != currentIndex)
		{
			// Anyway change current selection to new index
			object previousSelection = _currentSelection;
			_currentSelection = (newIndex >= 0 && newIndex < _resultsList.Count) ? _resultsList[newIndex] : null;

			if (Keyboard.current.leftShiftKey.isPressed)
			{
				// Special behaviour : expand or retract
				if (_selectedResults.Contains(previousSelection) && _selectedResults.Contains(_currentSelection) == false)
				{
					// Expand
					_selectedResults.Add(_currentSelection);
				}
				else if (_selectedResults.Contains(_currentSelection) && _selectedResults.Contains(previousSelection) && _selectedResults.Count > 1)
				{
					// Rectract
					_selectedResults.Remove(previousSelection);
				}
			}
			else
			{
				_selectedResults.Clear();
			}

			_selectedResults.Add(_currentSelection);
		}

		if (Keyboard.current.enterKey.isPressed || Keyboard.current.numpadEnterKey.isPressed && _selectedResults.Count > 0)
		{
			Close(SearchResult.Validate);
		}
		else if (Keyboard.current.escapeKey.isPressed && _currentCaption.Length == 0 && resultsRefreshed == false)
		{
			Close(SearchResult.Cancel);
		}
	}

	private void DrawFooter()
	{
		if (_handleMultiSelection)
		{
			GUI.enabled = _selectedResults.Count > 0;

			bool validated = GUILayout.Button("Validate");

			GUI.enabled = true;

			if (validated)
			{
				Close(SearchResult.Validate);
			}

		}
	}

	private void Close(SearchResult searchResult)
	{
		Close();

		object[] results = null;

		if (searchResult == SearchResult.Validate && _selectedResults.Count > 0)
		{
			results = new object[_selectedResults.Count];
			int index = 0;

			foreach (object result in _selectedResults)
			{
				results[index] = result;
				index++;
			}
		}
		else
		{
			searchResult = SearchResult.Cancel;
		}

		if (_handleMultiSelection)
		{
			if (_closedCallbackMulti != null)
			{
				_closedCallbackMulti(searchResult, results);
			}
		}
		else
		{
			if (_closedCallbackSingle != null)
			{
				_closedCallbackSingle(searchResult, results != null ? results[0] : null);
			}
		}

		_closedCallbackSingle = null;
		_closedCallbackMulti = null;
	}
	#endregion

	#region Imports
	[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
	public struct POINT
	{
		public int X;
		public int Y;

		public POINT(int x, int y)
		{
			this.X = x;
			this.Y = y;
		}
	}

	[System.Runtime.InteropServices.DllImport("user32.dll")]
	[return: System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)]
	static extern bool GetCursorPos(out POINT lpPoint);
	#endregion
}