using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text.RegularExpressions;
using Connected.Annotations;
using Connected.Extensions;
using Connected.Utilities;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;

namespace Connected.Components;

public partial class TimePicker : Picker<TimeSpan?>
{
	private const string format24Hours = "HH:mm";
	private const string format12Hours = "hh:mm tt";


	public TimePicker() : base()
	{
		_timeFormat = format24Hours;

		SetConverter(new LambdaConverter<TimeSpan?, string>((e) => OnSet(e), (e) => OnGet(e)));

		AdornmentIcon = Icons.Material.Filled.AccessTime;
		AdornmentAriaLabel = "Open Time Picker";
	}

	private string OnSet(TimeSpan? timespan)
	{
		if (timespan == null)
			return string.Empty;

		var time = DateTime.Today.Add(timespan.Value);

		return time.ToString(_timeFormat, Culture);
	}

	private TimeSpan? OnGet(string value)
	{
		if (string.IsNullOrEmpty(value))
			return null;

		if (DateTime.TryParseExact(value, _timeFormat, Culture, DateTimeStyles.None, out var time))
		{
			return time.TimeOfDay;
		}
		else
		{
			var m = Regex.Match(value, "AM|PM", RegexOptions.IgnoreCase);
			if (m.Success)
			{
				return DateTime.ParseExact(value, format12Hours, CultureInfo.InvariantCulture).TimeOfDay;
			}
			else
			{
				return DateTime.ParseExact(value, format24Hours, CultureInfo.InvariantCulture).TimeOfDay;
			}
		}
	}

	private bool _amPm = false;
	private OpenTo _currentView;
	private string _timeFormat = string.Empty;

	internal TimeSpan? TimeIntermediate { get; private set; }

	/// <summary>
	/// First view to show in the MudDatePicker.
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.PickerBehavior)]
	public OpenTo OpenTo { get; set; } = OpenTo.Hours;

	/// <summary>
	/// Choose the edition mode. By default, you can edit hours and minutes.
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.PickerBehavior)]
	public TimeEditMode TimeEditMode { get; set; } = TimeEditMode.Normal;

	/// <summary>
	/// Sets the amount of time in milliseconds to wait before closing the picker. This helps the user see that the time was selected before the popover disappears.
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.PickerBehavior)]
	public int ClosingDelay { get; set; } = 200;

	/// <summary>
	/// If AutoClose is set to true and PickerActions are defined, the hour and the minutes can be defined without any action.
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.PickerBehavior)]
	public bool AutoClose { get; set; }

	/// <summary>
	/// If true, sets 12 hour selection clock.
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.Behavior)]
	public bool AmPm
	{
		get => _amPm;
		set
		{
			if (value == _amPm)
				return;

			_amPm = value;

			_timeFormat = AmPm ? format12Hours : format24Hours;

			Modified = true;
			SetTextAsync(Converter.Convert(_value), false).AndForget();
		}
	}

	/// <summary>
	/// String Format for selected time view
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.Behavior)]
	public string TimeFormat
	{
		get => _timeFormat;
		set
		{
			if (_timeFormat == value)
				return;

			_timeFormat = value;
		
			Modified = true;
			SetTextAsync(Converter.Convert(_value), false).AndForget();
		}
	}

	/// <summary>
	/// The currently selected time (two-way bindable). If null, then nothing was selected.
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.Data)]
	public TimeSpan? Time
	{
		get => _value;
		set => SetTimeAsync(value, true).AndForget();
	}

	protected async Task SetTimeAsync(TimeSpan? time, bool updateValue)
	{
		if (_value != time)
		{
			TimeIntermediate = time;
			_value = time;
			if (updateValue)
				await SetTextAsync(Converter.Convert(_value), false);
			UpdateTimeSetFromTime();
			await TimeChanged.InvokeAsync(_value);
			BeginValidate();
			FieldChanged(_value);
		}
	}

	/// <summary>
	/// Fired when the date changes.
	/// </summary>
	[Parameter] public EventCallback<TimeSpan?> TimeChanged { get; set; }

	protected override Task StringValueChanged(string value)
	{
		Modified = true;
		// Update the time property (without updating back the Value property)
		return SetTimeAsync(Converter.ConvertBack(value), false);
	}

	//The last line cannot be tested
	[ExcludeFromCodeCoverage]
	protected override void OnPickerOpened()
	{
		base.OnPickerOpened();
		_currentView = TimeEditMode switch
		{
			TimeEditMode.Normal => OpenTo,
			TimeEditMode.OnlyHours => OpenTo.Hours,
			TimeEditMode.OnlyMinutes => OpenTo.Minutes,
			_ => _currentView
		};
	}

	protected internal override void Submit()
	{
		if (ReadOnly)
			return;
		Time = TimeIntermediate;
	}

	public override async void Clear(bool close = true)
	{
		TimeIntermediate = null;
		await SetTimeAsync(null, true);

		if (AutoClose == true)
		{
			Close(false);
		}
	}

	private string GetHourString()
	{
		if (TimeIntermediate == null)
			return "--";
		var h = AmPm ? TimeIntermediate.Value.ToAmPmHour() : TimeIntermediate.Value.Hours;
		return Math.Min(23, Math.Max(0, h)).ToString(CultureInfo.InvariantCulture);
	}

	private string GetMinuteString()
	{
		if (TimeIntermediate == null)
			return "--";
		return $"{Math.Min(59, Math.Max(0, TimeIntermediate.Value.Minutes)):D2}";
	}

	private void UpdateTime()
	{
		TimeIntermediate = new TimeSpan(_timeSet.Hour, _timeSet.Minute, 0);
		if ((PickerVariant == PickerVariant.Static && PickerActions == null) || (PickerActions != null && AutoClose))
		{
			Submit();
		}
	}

	private void OnHourClick()
	{
		_currentView = OpenTo.Hours;
		FocusAsync().AndForget();
	}

	private void OnMinutesClick()
	{
		_currentView = OpenTo.Minutes;
		FocusAsync().AndForget();
	}

	private void OnAmClicked()
	{
		_timeSet.Hour %= 12;  // "12:-- am" is "00:--" in 24h
		UpdateTime();
		FocusAsync().AndForget();
	}

	private void OnPmClicked()
	{
		if (_timeSet.Hour <= 12) // "12:-- pm" is "12:--" in 24h
			_timeSet.Hour += 12;
		_timeSet.Hour %= 24;
		UpdateTime();
		FocusAsync().AndForget();
	}

	protected string ToolbarClass =>
	new CssBuilder("picker-timepicker-toolbar")
	  .AddClass($"picker-timepicker-toolbar-landscape", Orientation == Orientation.Landscape && PickerVariant == PickerVariant.Static)
	  .AddClass(AdditionalClassList)
	.Build();

	protected string HoursButtonClass =>
	new CssBuilder("timepicker-button")
	  .AddClass($"timepicker-toolbar-text", _currentView == OpenTo.Minutes)
	.Build();

	protected string MinuteButtonClass =>
	new CssBuilder("timepicker-button")
	  .AddClass($"timepicker-toolbar-text", _currentView == OpenTo.Hours)
	.Build();

	protected string AmButtonClass =>
	new CssBuilder("timepicker-button")
	  .AddClass($"timepicker-toolbar-text", !IsAm) // gray it out
	.Build();

	protected string PmButtonClass =>
	new CssBuilder("timepicker-button")
	  .AddClass($"timepicker-toolbar-text", !IsPm) // gray it out
	.Build();

	private string HourDialClass =>
	new CssBuilder("time-picker-hour")
	  .AddClass($"time-picker-dial")
	  .AddClass($"time-picker-dial-out", _currentView != OpenTo.Hours)
	  .AddClass($"time-picker-dial-hidden", _currentView != OpenTo.Hours)
	.Build();

	private string MinuteDialClass =>
	new CssBuilder("time-picker-minute")
	  .AddClass($"time-picker-dial")
	  .AddClass($"time-picker-dial-out", _currentView != OpenTo.Minutes)
	  .AddClass($"time-picker-dial-hidden", _currentView != OpenTo.Minutes)
	.Build();

	private bool IsAm => _timeSet.Hour >= 00 && _timeSet.Hour < 12; // am is 00:00 to 11:59 
	private bool IsPm => _timeSet.Hour >= 12 && _timeSet.Hour < 24; // pm is 12:00 to 23:59 

	private string GetClockPinColor()
	{
		return $"picker-time-clock-pin mud-{Color.ToDescription()}";
	}

	private string GetClockPointerColor()
	{
		if (MouseDown)
			return $"picker-time-clock-pointer mud-{Color.ToDescription()}";
		else
			return $"picker-time-clock-pointer mud-picker-time-clock-pointer-animation mud-{Color.ToDescription()}";
	}

	private string GetClockPointerThumbColor()
	{
		var deg = GetDeg();
		if (deg % 30 == 0)
			return $"picker-time-clock-pointer-thumb mud-onclock-text mud-onclock-primary mud-{Color.ToDescription()}";
		else
			return $"picker-time-clock-pointer-thumb mud-onclock-minute mud-{Color.ToDescription()}-text";
	}

	private string GetNumberColor(int value)
	{
		if (_currentView == OpenTo.Hours)
		{
			var h = _timeSet.Hour;
			if (AmPm)
			{
				h = _timeSet.Hour % 12;
				if (_timeSet.Hour % 12 == 0)
					h = 12;
			}
			if (h == value)
				return $"clock-number mud-theme-{Color.ToDescription()}";
		}
		else if (_currentView == OpenTo.Minutes && _timeSet.Minute == value)
		{
			return $"clock-number mud-theme-{Color.ToDescription()}";
		}
		return $"clock-number";
	}

	private double GetDeg()
	{
		double deg = 0;
		if (_currentView == OpenTo.Hours)
			deg = (_timeSet.Hour * 30) % 360;
		if (_currentView == OpenTo.Minutes)
			deg = (_timeSet.Minute * 6) % 360;
		return deg;
	}

	private string GetTransform(double angle, double radius, double offsetX, double offsetY)
	{
		angle = angle / 180 * Math.PI;
		var x = (Math.Sin(angle) * radius + offsetX).ToString("F3", CultureInfo.InvariantCulture);
		var y = ((Math.Cos(angle) + 1) * radius + offsetY).ToString("F3", CultureInfo.InvariantCulture);
		return $"transform: translate({x}px, {y}px);";
	}

	private string GetPointerRotation()
	{
		double deg = 0;
		if (_currentView == OpenTo.Hours)
			deg = (_timeSet.Hour * 30) % 360;
		if (_currentView == OpenTo.Minutes)
			deg = (_timeSet.Minute * 6) % 360;
		return $"rotateZ({deg}deg);";
	}

	private string GetPointerHeight()
	{
		var height = 40;
		if (_currentView == OpenTo.Minutes)
			height = 40;
		if (_currentView == OpenTo.Hours)
		{
			if (!AmPm && _timeSet.Hour > 0 && _timeSet.Hour < 13)
				height = 26;
			else
				height = 40;
		}
		return $"{height}%;";
	}

	private readonly SetTime _timeSet = new();
	private int _initialHour;
	private int _initialMinute;

	protected override void OnInitialized()
	{
		base.OnInitialized();
		UpdateTimeSetFromTime();
		_currentView = OpenTo;
		_initialHour = _timeSet.Hour;
		_initialMinute = _timeSet.Minute;
	}


	private void UpdateTimeSetFromTime()
	{
		if (TimeIntermediate == null)
		{
			_timeSet.Hour = 0;
			_timeSet.Minute = 0;
			return;
		}
		_timeSet.Hour = TimeIntermediate.Value.Hours;
		_timeSet.Minute = TimeIntermediate.Value.Minutes;
	}

	public bool MouseDown { get; set; }

	/// <summary>
	/// Sets Mouse Down bool to true if mouse is inside the clock mask.
	/// </summary>
	private void OnMouseDown(MouseEventArgs e)
	{
		MouseDown = true;
	}

	/// <summary>
	/// Sets Mouse Down bool to false if mouse is inside the clock mask.
	/// </summary>
	private void OnMouseUp(MouseEventArgs e)
	{
		if (MouseDown && _currentView == OpenTo.Minutes && _timeSet.Minute != _initialMinute || _currentView == OpenTo.Hours && _timeSet.Hour != _initialHour && TimeEditMode == TimeEditMode.OnlyHours)
		{
			MouseDown = false;
			SubmitAndClose();
		}

		MouseDown = false;

		if (_currentView == OpenTo.Hours && _timeSet.Hour != _initialHour && TimeEditMode == TimeEditMode.Normal)
		{
			_currentView = OpenTo.Minutes;
		}
	}

	/// <summary>
	/// If MouseDown is true enables "dragging" effect on the clock pin/stick.
	/// </summary>
	private void OnMouseOverHour(int value)
	{
		if (MouseDown)
		{
			_timeSet.Hour = value;
			UpdateTime();
		}
	}

	/// <summary>
	/// On click for the hour "sticks", sets the hour.
	/// </summary>
	private void OnMouseClickHour(int value)
	{
		var h = value;
		if (AmPm)
		{
			if (IsAm && value == 12)
				h = 0;
			else if (IsPm && value < 12)
				h = value + 12;
		}
		_timeSet.Hour = h;
		UpdateTime();

		if (TimeEditMode == TimeEditMode.Normal)
		{
			_currentView = OpenTo.Minutes;
		}
		else if (TimeEditMode == TimeEditMode.OnlyHours)
		{
			SubmitAndClose();
		}
	}

	/// <summary>
	/// On mouse over for the minutes "sticks", sets the minute.
	/// </summary>
	private void OnMouseOverMinute(int value)
	{
		if (MouseDown)
		{
			_timeSet.Minute = value;
			UpdateTime();
		}
	}

	/// <summary>
	/// On click for the minute "sticks", sets the minute.
	/// </summary>
	private void OnMouseClickMinute(int value)
	{
		_timeSet.Minute = value;
		UpdateTime();
		SubmitAndClose();
	}

	protected async void SubmitAndClose()
	{
		if (PickerActions == null || AutoClose)
		{
			Submit();

			if (PickerVariant != PickerVariant.Static)
			{
				await Task.Delay(ClosingDelay);
				Close(false);
			}
		}
	}

	protected internal override void HandleKeyDown(KeyboardEventArgs obj)
	{
		if (Disabled || ReadOnly)
			return;
		base.HandleKeyDown(obj);
		switch (obj.Key)
		{
			case "ArrowRight":
				if (IsOpen)
				{
					if (obj.CtrlKey == true)
					{
						ChangeHour(1);
					}
					else if (obj.ShiftKey == true)
					{
						if (_timeSet.Minute > 55)
						{
							ChangeHour(1);
						}
						ChangeMinute(5);
					}
					else
					{
						if (_timeSet.Minute == 59)
						{
							ChangeHour(1);
						}
						ChangeMinute(1);
					}
				}
				break;
			case "ArrowLeft":
				if (IsOpen)
				{
					if (obj.CtrlKey == true)
					{
						ChangeHour(-1);
					}
					else if (obj.ShiftKey == true)
					{
						if (_timeSet.Minute < 5)
						{
							ChangeHour(-1);
						}
						ChangeMinute(-5);
					}
					else
					{
						if (_timeSet.Minute == 0)
						{
							ChangeHour(-1);
						}
						ChangeMinute(-1);
					}
				}
				break;
			case "ArrowUp":
				if (IsOpen == false && Editable == false)
				{
					IsOpen = true;
				}
				else if (obj.AltKey == true)
				{
					IsOpen = false;
				}
				else if (obj.ShiftKey == true)
				{
					ChangeHour(5);
				}
				else
				{
					ChangeHour(1);
				}
				break;
			case "ArrowDown":
				if (IsOpen == false && Editable == false)
				{
					IsOpen = true;
				}
				else if (obj.ShiftKey == true)
				{
					ChangeHour(-5);
				}
				else
				{
					ChangeHour(-1);
				}
				break;
			case "Escape":
				ReturnTimeBackUp();
				break;
			case "Enter":
			case "NumpadEnter":
				if (!IsOpen)
				{
					Open();
				}
				else
				{
					Submit();
					Close();
					_inputReference?.SetText(Text);
				}
				break;
			case " ":
				if (!Editable)
				{
					if (!IsOpen)
					{
						Open();
					}
					else
					{
						Submit();
						Close();
						_inputReference?.SetText(Text);
					}
				}
				break;
		}
	}

	protected void ChangeMinute(int val)
	{
		_currentView = OpenTo.Minutes;
		_timeSet.Minute = (_timeSet.Minute + val + 60) % 60;
		UpdateTime();
	}

	protected void ChangeHour(int val)
	{
		_currentView = OpenTo.Hours;
		_timeSet.Hour = (_timeSet.Hour + val + 24) % 24;
		UpdateTime();
	}

	protected void ReturnTimeBackUp()
	{
		if (Time == null)
		{
			TimeIntermediate = null;
		}
		else
		{
			_timeSet.Hour = Time.Value.Hours;
			_timeSet.Minute = Time.Value.Minutes;
			UpdateTime();
		}
	}

	private class SetTime
	{
		public int Hour { get; set; }

		public int Minute { get; set; }

	}
}
