// Copyright (c) MudBlazor 2021
// MudBlazor licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Diagnostics.CodeAnalysis;
using Connected.Annotations;
using Connected.Services;
using Connected.Utilities;
using Connected.Utilities.Exceptions;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;

namespace Connected.Components;

public partial class Select<T> : InputBase<T>, ISelect, IShadowSelect
{
	private HashSet<T> _selectedValues = new HashSet<T>();
	private IEqualityComparer<T> _comparer;
	private bool _dense;
	private string multiSelectionText;
	private bool? _selectAllChecked;
	private IKeyInterceptor _keyInterceptor;

	protected string Classname =>
	   new CssBuilder("select")
	   .AddClass(AdditionalClassList)
	   .Build();

	[Inject] private IKeyInterceptorFactory KeyInterceptorFactory { get; set; }
	[Inject] IScrollManager ScrollManager { get; set; }

	private string _elementId = "select_" + Guid.NewGuid().ToString().Substring(0, 8);

	private Task SelectNextItem() => SelectAdjacentItem(+1);

	private Task SelectPreviousItem() => SelectAdjacentItem(-1);

	private async Task SelectAdjacentItem(int direction)
	{
		if (_items == null || _items.Count == 0)
			return;
		var index = _items.FindIndex(x => x.ItemId == (string)_activeItemId);
		if (direction < 0 && index < 0)
			index = 0;
		SelectItem<T> item = null;
		// the loop allows us to jump over disabled items until we reach the next non-disabled one
		for (int i = 0; i < _items.Count; i++)
		{
			index += direction;
			if (index < 0)
				index = 0;
			if (index >= _items.Count)
				index = _items.Count - 1;
			if (_items[index].Disabled)
				continue;
			item = _items[index];
			if (!MultiSelection)
			{
				_selectedValues.Clear();
				_selectedValues.Add(item.Value);
				await SetValueAsync(item.Value, updateText: true);
				HilightItem(item);
				break;
			}
			else
			{
				// in multiselect mode don't select anything, just hilight.
				// selecting is done by Enter
				HilightItem(item);
				break;
			}
		}
		await _elementReference.SetText(Text);
		await ScrollToItemAsync(item);
	}
	private ValueTask ScrollToItemAsync(SelectItem<T> item)
	   => item != null ? ScrollManager.ScrollToListItemAsync(item.ItemId) : ValueTask.CompletedTask;
	private async Task SelectFirstItem(string startChar = null)
	{
		if (_items == null || _items.Count == 0)
			return;
		var items = _items.Where(x => !x.Disabled);
		var firstItem = items.FirstOrDefault();
		if (!string.IsNullOrWhiteSpace(startChar))
		{
			// find first item that starts with the letter
			var currentItem = items.FirstOrDefault(x => x.ItemId == (string)_activeItemId);
			if (currentItem != null &&
			   Converter.Convert(currentItem.Value)?.ToLowerInvariant().StartsWith(startChar) == true)
			{
				// this will step through all items that start with the same letter if pressed multiple times
				items = items.SkipWhile(x => x != currentItem).Skip(1);
			}
			items = items.Where(x => Converter.Convert(x.Value)?.ToLowerInvariant().StartsWith(startChar) == true);
		}
		var item = items.FirstOrDefault();
		if (item == null)
			return;
		if (!MultiSelection)
		{
			_selectedValues.Clear();
			_selectedValues.Add(item.Value);
			await SetValueAsync(item.Value, updateText: true);
			HilightItem(item);
		}
		else
		{
			HilightItem(item);
		}
		await _elementReference.SetText(Text);
		await ScrollToItemAsync(item);
	}

	private async Task SelectLastItem()
	{
		if (_items == null || _items.Count == 0)
			return;
		var item = _items.LastOrDefault(x => !x.Disabled);
		if (item == null)
			return;
		if (!MultiSelection)
		{
			_selectedValues.Clear();
			_selectedValues.Add(item.Value);
			await SetValueAsync(item.Value, updateText: true);
			HilightItem(item);
		}
		else
		{
			HilightItem(item);
		}
		await _elementReference.SetText(Text);
		await ScrollToItemAsync(item);
	}

	/// <summary>
	/// Fired when dropdown opens.
	/// </summary>
	[Category(CategoryTypes.FormComponent.Behavior)]
	[Parameter] public EventCallback OnOpen { get; set; }

	/// <summary>
	/// Fired when dropdown closes.
	/// </summary>
	[Category(CategoryTypes.FormComponent.Behavior)]
	[Parameter] public EventCallback OnClose { get; set; }

	/// <summary>
	/// Add the MudSelectItems here
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.ListBehavior)]
	public RenderFragment ChildContent { get; set; }

	/// <summary>
	/// User class names for the popover, separated by space
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.ListAppearance)]
	public string PopoverClass { get; set; }

	/// <summary>
	/// If true, compact vertical padding will be applied to all Select items.
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.ListAppearance)]
	public bool Dense
	{
		get { return _dense; }
		set { _dense = value; }
	}

	/// <summary>
	/// The Open Select Glyph
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.Appearance)]
	public string OpenIcon { get; set; } = Icons.Material.Filled.ArrowDropDown;

	/// <summary>
	/// The Close Select Glyph
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.Appearance)]
	public string CloseIcon { get; set; } = Icons.Material.Filled.ArrowDropUp;

	/// <summary>
	/// If set to true and the MultiSelection option is set to true, a "select all" checkbox is added at the top of the list of items.
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.ListBehavior)]
	public bool SelectAll { get; set; }

	/// <summary>
	/// Define the text of the Select All option.
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.ListAppearance)]
	public string SelectAllText { get; set; } = "Select all";

	/// <summary>
	/// Fires when SelectedValues changes.
	/// </summary>
	[Parameter] public EventCallback<IEnumerable<T>> SelectedValuesChanged { get; set; }

	/// <summary>
	/// Function to define a customized multiselection text.
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.Behavior)]
	public Func<List<string>, string> MultiSelectionTextFunc { get; set; }

	/// <summary>
	/// Parameter to define the delimited string separator.
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.Behavior)]
	public string Delimiter { get; set; } = ", ";

	/// <summary>
	/// Set of selected values. If MultiSelection is false it will only ever contain a single value. This property is two-way bindable.
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.Data)]
	public IEnumerable<T> SelectedValues
	{
		get
		{
			if (_selectedValues == null)
				_selectedValues = new HashSet<T>(_comparer);
			return _selectedValues;
		}
		set
		{
			var set = value ?? new HashSet<T>(_comparer);
			if (SelectedValues.Count() == set.Count() && _selectedValues.All(x => set.Contains(x)))
				return;
			_selectedValues = new HashSet<T>(set, _comparer);
			SelectionChangedFromOutside?.Invoke(_selectedValues);
			if (!MultiSelection)
				SetValueAsync(_selectedValues.FirstOrDefault()).AndForget();
			else
			{
				//Warning. Here the Converter was not set yet
				if (MultiSelectionTextFunc != null)
				{
					SetCustomizedTextAsync(string.Join(Delimiter, SelectedValues.Select(x => Converter.Convert(x))),
					   selectedConvertedValues: SelectedValues.Select(x => Converter.Convert(x)).ToList(),
					   multiSelectionTextFunc: MultiSelectionTextFunc).AndForget();
				}
				else
				{
					SetTextAsync(string.Join(Delimiter, SelectedValues.Select(x => Converter.Convert(x))), updateValue: false).AndForget();
				}
			}
			SelectedValuesChanged.InvokeAsync(new HashSet<T>(SelectedValues, _comparer));
			if (MultiSelection && typeof(T) == typeof(string))
				SetValueAsync((T)(object)Text, updateText: false).AndForget();
		}
	}

	/// <summary>
	/// The Comparer to use for comparing selected values internally.
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.Behavior)]
	public IEqualityComparer<T> Comparer
	{
		get => _comparer;
		set
		{
			_comparer = value;
			// Apply comparer and refresh selected values
			_selectedValues = new HashSet<T>(_selectedValues, _comparer);
			SelectedValues = _selectedValues;
		}
	}

	private Func<T, string> _toStringFunc = x => x?.ToString();

	private Input<string> _elementReference;

	/// <summary>
	/// Defines how values are displayed in the drop-down list
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.ListBehavior)]
	public Func<T, string> ToStringFunc
	{
		get => _toStringFunc;
		set
		{
			if (_toStringFunc == value)
				return;
			_toStringFunc = value;
			SetConverter(new LambdaConverter<T, string>(_toStringFunc ?? (x => x?.ToString()), null));
		}
	}

	public Select()
	{
		Adornment = Adornment.End;
		IconSize = Size.Medium;
	}

	protected override void OnAfterRender(bool firstRender)
	{
		base.OnAfterRender(firstRender);
		if (firstRender && Value != null)
		{
			// we need to render the initial Value which is not possible without the items
			// which supply the RenderFragment. So in this case, a second render is necessary
			StateHasChanged();
		}
		UpdateSelectAllChecked();
		lock (this)
		{
			if (_renderComplete != null)
			{
				_renderComplete.TrySetResult();
				_renderComplete = null;
			}
		}
	}


	private Task WaitForRender()
	{
		Task t = null;
		lock (this)
		{
			if (_renderComplete != null)
				return _renderComplete.Task;
			_renderComplete = new TaskCompletionSource();
			t = _renderComplete.Task;
		}
		StateHasChanged();
		return t;
	}

	private TaskCompletionSource _renderComplete;

	/// <summary>
	/// Returns whether or not the Value can be found in items. If not, the Select will display it as a string.
	/// </summary>
	protected bool CanRenderValue
	{
		get
		{
			if (Value == null)
				return false;
			if (!_shadowLookup.TryGetValue(Value, out var item))
				return false;
			return (item.ChildContent != null);
		}
	}

	protected bool IsValueInList
	{
		get
		{
			if (Value == null)
				return false;
			return _shadowLookup.TryGetValue(Value, out var _);
		}
	}

	protected RenderFragment GetSelectedValuePresenter()
	{
		if (Value == null)
			return null;
		if (!_shadowLookup.TryGetValue(Value, out var item))
			return null; //<-- for now. we'll add a custom template to present values (set from outside) which are not on the list?
		return item.ChildContent;
	}

	protected override Task UpdateValuePropertyAsync(bool updateText)
	{
		// For MultiSelection of non-string T's we don't update the Value!!!
		if (typeof(T) == typeof(string) || !MultiSelection)
			base.UpdateValuePropertyAsync(updateText);
		return Task.CompletedTask;
	}

	protected override Task UpdateTextPropertyAsync(bool updateValue)
	{
		// when multiselection is true, we return
		// a comma separated list of selected values
		if (MultiSelectionTextFunc != null)
		{
			return MultiSelection
			   ? SetCustomizedTextAsync(string.Join(Delimiter, SelectedValues.Select(x => Converter.Convert(x))),
				  selectedConvertedValues: SelectedValues.Select(x => Converter.Convert(x)).ToList(),
				  multiSelectionTextFunc: MultiSelectionTextFunc)
			   : base.UpdateTextPropertyAsync(updateValue);
		}
		else
		{
			return MultiSelection
			   ? SetTextAsync(string.Join(Delimiter, SelectedValues.Select(x => Converter.Convert(x))))
			   : base.UpdateTextPropertyAsync(updateValue);
		}
	}

	internal event Action<ICollection<T>> SelectionChangedFromOutside;

	private bool _multiSelection;
	/// <summary>
	/// If true, multiple values can be selected via checkboxes which are automatically shown in the dropdown
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.ListBehavior)]
	public bool MultiSelection
	{
		get => _multiSelection;
		set
		{
			if (value != _multiSelection)
			{
				_multiSelection = value;
				UpdateTextPropertyAsync(false).AndForget();
			}
		}
	}

	/// <summary>
	/// The collection of items within this select
	/// </summary>
	public IReadOnlyList<SelectItem<T>> Items => _items;

	protected internal List<SelectItem<T>> _items = new();
	protected Dictionary<T, SelectItem<T>> _valueLookup = new();
	protected Dictionary<T, SelectItem<T>> _shadowLookup = new();

	// note: this must be object to satisfy MudList
	private object _activeItemId = null;

	internal bool Add(SelectItem<T> item)
	{
		if (item == null)
			return false;
		bool? result = null;
		if (!_items.Select(x => x.Value).Contains(item.Value))
		{
			_items.Add(item);

			if (item.Value != null)
			{
				_valueLookup[item.Value] = item;
				if (item.Value.Equals(Value) && !MultiSelection)
					result = true;
			}
		}
		UpdateSelectAllChecked();
		if (result.HasValue == false)
		{
			result = item.Value?.Equals(Value);
		}
		return result == true;
	}

	internal void Remove(SelectItem<T> item)
	{
		_items.Remove(item);
		if (item.Value != null)
			_valueLookup.Remove(item.Value);
	}

	/// <summary>
	/// Sets the maxheight the Select can have when open.
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.ListAppearance)]
	public int MaxHeight { get; set; } = 300;

	/// <summary>
	/// Set the anchor origin point to determen where the popover will open from.
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.ListAppearance)]
	public Origin AnchorOrigin { get; set; } = Origin.TopCenter;

	/// <summary>
	/// Sets the transform origin point for the popover.
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.ListAppearance)]
	public Origin TransformOrigin { get; set; } = Origin.TopCenter;

	/// <summary>
	/// Sets the direction the Select menu should open.
	/// </summary>
	[ExcludeFromCodeCoverage]
	[Obsolete("Use AnchorOrigin or TransformOrigin instead.", true)]
	[Parameter] public Direction Direction { get; set; } = Direction.Bottom;

	/// <summary>
	/// If true, the Select menu will open either before or after the input (left/right).
	/// </summary>
	[ExcludeFromCodeCoverage]
	[Obsolete("Use AnchorOrigin or TransformOrigin instead.", true)]
	[Parameter] public bool OffsetX { get; set; }

	/// <summary>
	/// If true, the Select menu will open either before or after the input (top/bottom).
	/// </summary>
	/// [ExcludeFromCodeCoverage]
	[Obsolete("Use AnchorOrigin or TransformOrigin instead.", true)]
	[Parameter] public bool OffsetY { get; set; }

	/// <summary>
	/// If true, the Select's input will not show any values that are not defined in the dropdown.
	/// This can be useful if Value is bound to a variable which is initialized to a value which is not in the list
	/// and you want the Select to show the label / placeholder instead.
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.Behavior)]
	public bool Strict { get; set; }

	/// <summary>
	/// Show clear button.
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.Behavior)]
	public bool Clearable { get; set; } = false;

	/// <summary>
	/// If true, prevent scrolling while dropdown is open.
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.ListBehavior)]
	public bool LockScroll { get; set; } = false;

	/// <summary>
	/// Button click event for clear button. Called after text and value has been cleared.
	/// </summary>
	[Parameter] public EventCallback<MouseEventArgs> OnClearButtonClick { get; set; }

	internal bool _isOpen;

	public string _currentIcon { get; set; }

	public async Task SelectOption(int index)
	{
		if (index < 0 || index >= _items.Count)
		{
			if (!MultiSelection)
				await CloseMenu();
			return;
		}
		await SelectOption(_items[index].Value);
	}

	public async Task SelectOption(object obj)
	{
		var value = (T)obj;
		if (MultiSelection)
		{
			// multi-selection: menu stays open
			if (!_selectedValues.Contains(value))
				_selectedValues.Add(value);
			else
				_selectedValues.Remove(value);

			if (MultiSelectionTextFunc != null)
			{
				await SetCustomizedTextAsync(string.Join(Delimiter, SelectedValues.Select(x => Converter.Convert(x))),
				   selectedConvertedValues: SelectedValues.Select(x => Converter.Convert(x)).ToList(),
				   multiSelectionTextFunc: MultiSelectionTextFunc);
			}
			else
			{
				await SetTextAsync(string.Join(Delimiter, SelectedValues.Select(x => Converter.Convert(x))), updateValue: false);
			}

			UpdateSelectAllChecked();
			BeginValidate();
		}
		else
		{
			// single selection
			// CloseMenu(true) doesn't close popover in BSS
			await CloseMenu(false);

			if (EqualityComparer<T>.Default.Equals(Value, value))
			{
				StateHasChanged();
				return;
			}

			await SetValueAsync(value);
			_elementReference.SetText(Text).AndForget();
			_selectedValues.Clear();
			_selectedValues.Add(value);
		}

		HilightItemForValue(value);
		await SelectedValuesChanged.InvokeAsync(SelectedValues);
		if (MultiSelection && typeof(T) == typeof(string))
			await SetValueAsync((T)(object)Text, updateText: false);
		await InvokeAsync(StateHasChanged);
	}

	private async void HilightItemForValue(T value)
	{
		if (value == null)
		{
			HilightItem(null);
			return;
		}
		await WaitForRender();
		_valueLookup.TryGetValue(value, out var item);
		HilightItem(item);
	}

	private async void HilightItem(SelectItem<T> item)
	{
		_activeItemId = item?.ItemId;
		// we need to make sure we are just after a render here or else there will be race conditions
		await WaitForRender();
		// Note: this is a hack but I found no other way to make the list hilight the currently hilighted item
		// without the delay it always shows the previously hilighted item because the popup items don't exist yet
		// they are only registered after they are rendered, so we need to render again!
		await Task.Delay(1);
		StateHasChanged();
	}

	private async Task HilightSelectedValue()
	{
		await WaitForRender();
		if (MultiSelection)
			HilightItem(_items.FirstOrDefault(x => !x.Disabled));
		else
			HilightItemForValue(Value);
	}

	private void UpdateSelectAllChecked()
	{
		if (MultiSelection && SelectAll)
		{
			var oldState = _selectAllChecked;
			if (_selectedValues.Count == 0)
			{
				_selectAllChecked = false;
			}
			else if (_items.Count == _selectedValues.Count)
			{
				_selectAllChecked = true;
			}
			else
			{
				_selectAllChecked = null;
			}
		}
	}

	public async Task ToggleMenu()
	{
		if (Disabled || ReadOnly)
			return;
		if (_isOpen)
			await CloseMenu(true);
		else
			await OpenMenu();
	}

	public async Task OpenMenu()
	{
		if (Disabled || ReadOnly)
			return;
		_isOpen = true;
		UpdateIcon();
		StateHasChanged();
		await HilightSelectedValue();
		//Scroll the active item on each opening
		if (_activeItemId != null)
		{
			var index = _items.FindIndex(x => x.ItemId == (string)_activeItemId);
			if (index > 0)
			{
				var item = _items[index];
				await ScrollToItemAsync(item);
			}
		}
		//disable escape propagation: if selectmenu is open, only the select popover should close and underlying components should not handle escape key
		await _keyInterceptor.UpdateKey(new() { Key = "Escape", StopDown = "Key+none" });

		await OnOpen.InvokeAsync();
	}

	public async Task CloseMenu(bool focusAgain = true)
	{
		_isOpen = false;
		UpdateIcon();
		if (focusAgain == true)
		{
			StateHasChanged();
			await OnBlur.InvokeAsync(new FocusEventArgs());
			_elementReference.FocusAsync().AndForget(TaskOption.Safe);
			StateHasChanged();
		}

		//enable escape propagation: the select popover was closed, now underlying components are allowed to handle escape key
		await _keyInterceptor.UpdateKey(new() { Key = "Escape", StopDown = "none" });

		await OnClose.InvokeAsync();
	}

	private void UpdateIcon()
	{
		_currentIcon = !string.IsNullOrWhiteSpace(AdornmentIcon) ? AdornmentIcon : _isOpen ? CloseIcon : OpenIcon;
	}

	protected override void OnInitialized()
	{
		base.OnInitialized();
		UpdateIcon();
	}

	protected override void OnParametersSet()
	{
		base.OnParametersSet();
		UpdateIcon();
	}

	protected override async Task OnAfterRenderAsync(bool firstRender)
	{
		if (firstRender)
		{
			_keyInterceptor = KeyInterceptorFactory.Create();

			await _keyInterceptor.Connect(_elementId, new KeyInterceptorOptions()
			{
				//EnableLogging = true,
				TargetClass = "input-control",
				Keys = {
				new KeyOptions { Key=" ", PreventDown = "key+none" }, //prevent scrolling page, toggle open/close
                     new KeyOptions { Key="ArrowUp", PreventDown = "key+none" }, // prevent scrolling page, instead hilight previous item
                     new KeyOptions { Key="ArrowDown", PreventDown = "key+none" }, // prevent scrolling page, instead hilight next item
                     new KeyOptions { Key="Home", PreventDown = "key+none" },
				new KeyOptions { Key="End", PreventDown = "key+none" },
				new KeyOptions { Key="Escape" },
				new KeyOptions { Key="Enter", PreventDown = "key+none" },
				new KeyOptions { Key="NumpadEnter", PreventDown = "key+none" },
				new KeyOptions { Key="a", PreventDown = "key+ctrl" }, // select all items instead of all page text
                     new KeyOptions { Key="A", PreventDown = "key+ctrl" }, // select all items instead of all page text
                     new KeyOptions { Key="/./", SubscribeDown = true, SubscribeUp = true }, // for our users
                 },
			});
			_keyInterceptor.KeyDown += HandleKeyDown;
			_keyInterceptor.KeyUp += HandleKeyUp;
		}

		await base.OnAfterRenderAsync(firstRender);
	}

	public void CheckGenericTypeMatch(object select_item)
	{
		var itemT = select_item.GetType().GenericTypeArguments[0];
		if (itemT != typeof(T))
			throw new GenericTypeMismatchException("MudSelect", "MudSelectItem", typeof(T), itemT);
	}

	public override ValueTask FocusAsync()
	{
		return _elementReference.FocusAsync();
	}

	public override ValueTask BlurAsync()
	{
		return _elementReference.BlurAsync();
	}

	public override ValueTask SelectAsync()
	{
		return _elementReference.SelectAsync();
	}

	public override ValueTask SelectRangeAsync(int pos1, int pos2)
	{
		return _elementReference.SelectRangeAsync(pos1, pos2);
	}

	/// <summary>
	/// Extra handler for clearing selection.
	/// </summary>
	protected async ValueTask SelectClearButtonClickHandlerAsync(MouseEventArgs e)
	{
		await SetValueAsync(default, false);
		await SetTextAsync(default, false);
		_selectedValues.Clear();
		BeginValidate();
		StateHasChanged();
		await SelectedValuesChanged.InvokeAsync(_selectedValues);
		await OnClearButtonClick.InvokeAsync(e);
	}

	protected async Task SetCustomizedTextAsync(string text, bool updateValue = true,
	   List<string> selectedConvertedValues = null,
	   Func<List<string>, string> multiSelectionTextFunc = null)
	{
		// The Text property of the control is updated
		Text = multiSelectionTextFunc?.Invoke(selectedConvertedValues);

		// The comparison is made on the multiSelectionText variable
		if (multiSelectionText != text)
		{
			multiSelectionText = text;
			if (!string.IsNullOrWhiteSpace(multiSelectionText))
				Modified = true;
			if (updateValue)
				await UpdateValuePropertyAsync(false);
			await TextChanged.InvokeAsync(multiSelectionText);
		}
	}

	/// <summary>
	/// Custom checked icon.
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.ListAppearance)]
	public string CheckedIcon { get; set; } = Icons.Material.Filled.CheckBox;

	/// <summary>
	/// Custom unchecked icon.
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.ListAppearance)]
	public string UncheckedIcon { get; set; } = Icons.Material.Filled.CheckBoxOutlineBlank;

	/// <summary>
	/// Custom indeterminate icon.
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.ListAppearance)]
	public string IndeterminateIcon { get; set; } = Icons.Material.Filled.IndeterminateCheckBox;

	/// <summary>
	/// The checkbox icon reflects the select all option's state
	/// </summary>
	protected string SelectAllCheckBoxIcon
	{
		get
		{
			return _selectAllChecked.HasValue ? _selectAllChecked.Value ? CheckedIcon : UncheckedIcon : IndeterminateIcon;
		}
	}

	internal async void HandleKeyDown(KeyboardEventArgs obj)
	{
		if (Disabled || ReadOnly)
			return;
		var key = obj.Key.ToLowerInvariant();
		if (_isOpen && key.Length == 1 && key != " " && !(obj.CtrlKey || obj.ShiftKey || obj.AltKey || obj.MetaKey))
		{
			await SelectFirstItem(key);
			return;
		}
		switch (obj.Key)
		{
			case "Tab":
				await CloseMenu(false);
				break;
			case "ArrowUp":
				if (obj.AltKey == true)
				{
					await CloseMenu();
					break;
				}
				else if (_isOpen == false)
				{
					await OpenMenu();
					break;
				}
				else
				{
					await SelectPreviousItem();
					break;
				}
			case "ArrowDown":
				if (obj.AltKey == true)
				{
					await OpenMenu();
					break;
				}
				else if (_isOpen == false)
				{
					await OpenMenu();
					break;
				}
				else
				{
					await SelectNextItem();
					break;
				}
			case " ":
				await ToggleMenu();
				break;
			case "Escape":
				await CloseMenu(true);
				break;
			case "Home":
				await SelectFirstItem();
				break;
			case "End":
				await SelectLastItem();
				break;
			case "Enter":
			case "NumpadEnter":
				var index = _items.FindIndex(x => x.ItemId == (string)_activeItemId);
				if (!MultiSelection)
				{
					if (!_isOpen)
					{
						await OpenMenu();
						return;
					}
					// this also closes the menu
					await SelectOption(index);
					break;
				}
				else
				{
					if (_isOpen == false)
					{
						await OpenMenu();
						break;
					}
					else
					{
						await SelectOption(index);
						await _elementReference.SetText(Text);
						break;
					}
				}
			case "a":
			case "A":
				if (obj.CtrlKey == true)
				{
					if (MultiSelection)
					{
						await SelectAllClickAsync();
						//If we didn't add delay, it won't work.
						await WaitForRender();
						await Task.Delay(1);
						StateHasChanged();
						//It only works when selecting all, not render unselect all.
						//UpdateSelectAllChecked();
					}
				}
				break;
		}
		OnKeyDown.InvokeAsync(obj).AndForget();

	}

	internal void HandleKeyUp(KeyboardEventArgs obj)
	{
		OnKeyUp.InvokeAsync(obj).AndForget();
	}

	[ExcludeFromCodeCoverage]
	[Obsolete("Use Clear instead.", true)]
	public Task ClearAsync() => Clear();

	/// <summary>
	/// Clear the selection
	/// </summary>
	public async Task Clear()
	{
		await SetValueAsync(default, false);
		await SetTextAsync(default, false);
		_selectedValues.Clear();
		BeginValidate();
		StateHasChanged();
		await SelectedValuesChanged.InvokeAsync(_selectedValues);
	}

	private async Task SelectAllClickAsync()
	{
		// Manage the fake tri-state of a checkbox
		if (!_selectAllChecked.HasValue)
			_selectAllChecked = true;
		else if (_selectAllChecked.Value)
			_selectAllChecked = false;
		else
			_selectAllChecked = true;
		// Define the items selection
		if (_selectAllChecked.Value == true)
			await SelectAllItems();
		else
			await Clear();
	}

	private async Task SelectAllItems()
	{
		if (!MultiSelection)
			return;
		var selectedValues = new HashSet<T>(_items.Where(x => !x.Disabled && x.Value != null).Select(x => x.Value), _comparer);
		_selectedValues = new HashSet<T>(selectedValues, _comparer);
		if (MultiSelectionTextFunc != null)
		{
			await SetCustomizedTextAsync(string.Join(Delimiter, SelectedValues.Select(x => Converter.Convert(x))),
			   selectedConvertedValues: SelectedValues.Select(x => Converter.Convert(x)).ToList(),
			   multiSelectionTextFunc: MultiSelectionTextFunc);
		}
		else
		{
			await SetTextAsync(string.Join(Delimiter, SelectedValues.Select(x => Converter.Convert(x))), updateValue: false);
		}
		UpdateSelectAllChecked();
		_selectedValues = selectedValues; // need to force selected values because Blazor overwrites it under certain circumstances due to changes of Text or Value
		BeginValidate();
		await SelectedValuesChanged.InvokeAsync(SelectedValues);
		if (MultiSelection && typeof(T) == typeof(string))
			SetValueAsync((T)(object)Text, updateText: false).AndForget();
	}

	public void RegisterShadowItem(SelectItem<T> item)
	{
		if (item == null || item.Value == null)
			return;
		_shadowLookup[item.Value] = item;
	}

	public void UnregisterShadowItem(SelectItem<T> item)
	{
		if (item == null || item.Value == null)
			return;
		_shadowLookup.Remove(item.Value);
	}

	internal void OnLostFocus(FocusEventArgs obj)
	{
		if (_isOpen)
		{
			// when the menu is open we immediately get back the focus if we lose it (i.e. because of checkboxes in multi-select)
			// otherwise we can't receive key strokes any longer
			_elementReference.FocusAsync().AndForget(TaskOption.Safe);
		}
		base.OnBlur.InvokeAsync(obj);
	}

	protected override void Dispose(bool disposing)
	{
		base.Dispose(disposing);

		if (disposing == true)
		{
			if (_keyInterceptor != null)
			{
				_keyInterceptor.KeyDown -= HandleKeyDown;
				_keyInterceptor.KeyUp -= HandleKeyUp;

				_keyInterceptor.Dispose();
			}
		}
	}

	/// <summary>
	/// Fixes issue #4328
	/// Returns true when MultiSelection is true and it has selected values(Since Value property is not used when MultiSelection=true
	/// </summary>
	/// <param name="value"></param>
	/// <returns>True when component has a value</returns>
	protected override bool HasValue(T value)
	{
		if (MultiSelection)
			return SelectedValues?.Count() > 0;
		else
			return base.HasValue(value);
	}
}
