using System.Globalization;
using Connected.Annotations;
using Connected.Extensions;
using Connected.Utilities;
using Microsoft.AspNetCore.Components;

namespace Connected.Components;

public abstract partial class DatePickerBase : Picker<DateTime?>
{
	private bool _dateFormatTouched;

	protected DatePickerBase() : base(new DefaultConverter<DateTime?>
	{
		Format = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern,
		Culture = CultureInfo.CurrentCulture
	})
	{
		AdornmentAriaLabel = "Open Date Picker";
	}

	[Inject] protected IScrollManager ScrollManager { get; set; }

	/// <summary>
	/// Max selectable date.
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.Validation)]
	public DateTime? MaxDate { get; set; }

	/// <summary>
	/// Min selectable date.
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.Validation)]
	public DateTime? MinDate { get; set; }

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

	/// <summary>
	/// String Format for selected date view
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.Behavior)]
	public string DateFormat
	{
		get
		{
			return (Converter as DefaultConverter<DateTime?>)?.Format;
		}
		set
		{
			if (Converter is DefaultConverter<DateTime?> defaultConverter)
			{
				defaultConverter.Format = value;
				_dateFormatTouched = true;
			}
			DateFormatChanged(value);
		}
	}

	/// <summary>
	/// Date format value change hook for descendants.
	/// </summary>
	protected virtual Task DateFormatChanged(string newFormat)
	{
		return Task.CompletedTask;
	}

	protected override bool SetCulture(CultureInfo value)
	{
		if (!base.SetCulture(value))
			return false;

		if (!_dateFormatTouched && Converter is DefaultConverter<DateTime?> defaultConverter)
			defaultConverter.Format = value.DateTimeFormat.ShortDatePattern;

		return true;
	}

	/// <summary>
	/// Defines on which day the week starts. Depends on the value of Culture. 
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.PickerBehavior)]
	public DayOfWeek? FirstDayOfWeek { get; set; } = null;

	/// <summary>
	/// The current month of the date picker (two-way bindable). This changes when the user browses through the calender.
	/// The month is represented as a DateTime which is always the first day of that month. You can also set this to define which month is initially shown. If not set, the current month is shown.
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.PickerBehavior)]
	public DateTime? PickerMonth
	{
		get => _picker_month;
		set
		{
			if (value == _picker_month)
				return;
			_picker_month = value;
			InvokeAsync(StateHasChanged);
			PickerMonthChanged.InvokeAsync(value);
		}
	}

	private DateTime? _picker_month;

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

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

	/// <summary>
	/// Number of months to display in the calendar
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.PickerBehavior)]
	public int DisplayMonths { get; set; } = 1;

	/// <summary>
	/// Maximum number of months in one row
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.PickerAppearance)]
	public int? MaxMonthColumns { get; set; }

	/// <summary>
	/// Start month when opening the picker. 
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.PickerBehavior)]
	public DateTime? StartMonth { get; set; }

	/// <summary>
	/// Display week numbers according to the Culture parameter. If no culture is defined, CultureInfo.CurrentCulture will be used.
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.PickerBehavior)]
	public bool ShowWeekNumbers { get; set; }

	/// <summary>
	/// Format of the selected date in the title. By default, this is "ddd, dd MMM" which abbreviates day and month names. 
	/// For instance, display the long names like this "dddd, dd. MMMM". 
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.PickerBehavior)]
	public string TitleDateFormat { get; set; } = "ddd, dd MMM";

	/// <summary>
	/// If AutoClose is set to true and PickerActions are defined, selecting a day will close the MudDatePicker.
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.PickerBehavior)]
	public bool AutoClose { get; set; }

	/// <summary>
	/// Function to determine whether a date is disabled
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.Validation)]
	public Func<DateTime, bool> IsDateDisabledFunc
	{
		get => _isDateDisabledFunc;
		set
		{
			_isDateDisabledFunc = value ?? (_ => false);
		}
	}
	private Func<DateTime, bool> _isDateDisabledFunc = _ => false;

	/// <summary>
	/// Function to conditionally apply new classes to specific days
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.Appearance)]
	public Func<DateTime, string> AdditionalDateClassesFunc { get; set; }

	/// <summary>
	/// Custom previous icon.
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.PickerAppearance)]
	public string PreviousIcon { get; set; } = Icons.Material.Filled.ChevronLeft;

	/// <summary>
	/// Custom next icon.
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.PickerAppearance)]
	public string NextIcon { get; set; } = Icons.Material.Filled.ChevronRight;

	/// <summary>
	/// Set a predefined fix year - no year can be selected
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.PickerBehavior)]
	public int? FixYear { get; set; }
	/// <summary>
	/// Set a predefined fix month - no month can be selected
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.PickerBehavior)]
	public int? FixMonth { get; set; }
	/// <summary>
	/// Set a predefined fix day - no day can be selected
	/// </summary>
	[Parameter]
	[Category(CategoryTypes.FormComponent.PickerBehavior)]
	public int? FixDay { get; set; }

	protected virtual bool IsRange { get; } = false;

	protected OpenTo CurrentView;

	protected override void OnPickerOpened()
	{
		base.OnPickerOpened();
		if (Editable == true && Text != null)
		{
			DateTime? a = Converter.ConvertBack(Text);
			if (a.HasValue)
			{
				a = new DateTime(a.Value.Year, a.Value.Month, 1);
				PickerMonth = a;
			}
		}
		if (OpenTo == OpenTo.Date && FixDay.HasValue)
		{
			OpenTo = OpenTo.Month;
		}
		if (OpenTo == OpenTo.Date && FixDay.HasValue && FixMonth.HasValue)
		{
			OpenTo = OpenTo.Year;
		}
		CurrentView = OpenTo;
		if (CurrentView == OpenTo.Year)
			_scrollToYearAfterRender = true;
	}

	/// <summary>
	/// Get the first of the month to display
	/// </summary>
	/// <returns></returns>
	protected DateTime GetMonthStart(int month)
	{
		var monthStartDate = _picker_month ?? DateTime.Today.StartOfMonth(Culture);
		// Return the min supported datetime of the calendar when this is year 1 and first month!
		if (_picker_month.HasValue && _picker_month.Value.Year == 1 && _picker_month.Value.Month == 1)
		{
			return Culture.Calendar.MinSupportedDateTime;
		}
		return Culture.Calendar.AddMonths(monthStartDate, month);
	}

	/// <summary>
	/// Get the last of the month to display
	/// </summary>
	/// <returns></returns>
	protected DateTime GetMonthEnd(int month)
	{
		var monthStartDate = _picker_month ?? DateTime.Today.StartOfMonth(Culture);
		return Culture.Calendar.AddMonths(monthStartDate, month).EndOfMonth(Culture);
	}

	protected DayOfWeek GetFirstDayOfWeek()
	{
		if (FirstDayOfWeek.HasValue)
			return FirstDayOfWeek.Value;
		return Culture.DateTimeFormat.FirstDayOfWeek;
	}

	/// <summary>
	/// Gets the n-th week of the currently displayed month. 
	/// </summary>
	/// <param name="month">offset from _picker_month</param>
	/// <param name="index">between 0 and 4</param>
	/// <returns></returns>
	protected IEnumerable<DateTime> GetWeek(int month, int index)
	{
		if (index is < 0 or > 5)
			throw new ArgumentException("Index must be between 0 and 5");
		var month_first = GetMonthStart(month);
		var week_first = month_first.AddDays(index * 7).StartOfWeek(GetFirstDayOfWeek());
		for (var i = 0; i < 7; i++)
			yield return week_first.AddDays(i);
	}

	private string GetWeekNumber(int month, int index)
	{
		if (index is < 0 or > 5)
			throw new ArgumentException("Index must be between 0 and 5");
		var month_first = GetMonthStart(month);
		var week_first = month_first.AddDays(index * 7).StartOfWeek(GetFirstDayOfWeek());
		//january 1st
		if (month_first.Month == 1 && index == 0)
		{
			week_first = month_first;
		}

		if (week_first.Month != month_first.Month && week_first.AddDays(6).Month != month_first.Month)
			return "";

		return Culture.Calendar.GetWeekOfYear(week_first,
			Culture.DateTimeFormat.CalendarWeekRule, FirstDayOfWeek ?? Culture.DateTimeFormat.FirstDayOfWeek).ToString();
	}

	protected virtual OpenTo? GetNextView()
	{
		OpenTo? nextView = CurrentView switch
		{
			OpenTo.Year => !FixMonth.HasValue ? OpenTo.Month : !FixDay.HasValue ? OpenTo.Date : null,
			OpenTo.Month => !FixDay.HasValue ? OpenTo.Date : null,
			_ => null,
		};
		return nextView;
	}

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

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

	protected abstract string GetDayClasses(int month, DateTime day);

	/// <summary>
	/// User clicked on a day
	/// </summary>
	protected abstract void OnDayClicked(DateTime dateTime);

	/// <summary>
	/// user clicked on a month
	/// </summary>
	/// <param name="month"></param>
	protected virtual void OnMonthSelected(DateTime month)
	{
		PickerMonth = month;
		var nextView = GetNextView();
		if (nextView != null)
		{
			CurrentView = (OpenTo)nextView;
		}
	}

	/// <summary>
	/// user clicked on a year
	/// </summary>
	/// <param name="year"></param>
	protected virtual void OnYearClicked(int year)
	{
		var current = GetMonthStart(0);
		PickerMonth = new DateTime(year, current.Month, 1);
		var nextView = GetNextView();
		if (nextView != null)
		{
			CurrentView = (OpenTo)nextView;
		}
	}

	/// <summary>
	/// user clicked on a month
	/// </summary>
	protected virtual void OnMonthClicked(int month)
	{
		CurrentView = OpenTo.Month;
		_picker_month = _picker_month?.AddMonths(month);
		StateHasChanged();
	}

	/// <summary>
	/// return Mo, Tu, We, Th, Fr, Sa, Su in the right culture
	/// </summary>
	/// <returns></returns>
	protected IEnumerable<string> GetAbbreviatedDayNames()
	{
		var dayNamesNormal = Culture.DateTimeFormat.AbbreviatedDayNames;
		var dayNamesShifted = Shift(dayNamesNormal, (int)GetFirstDayOfWeek());
		return dayNamesShifted;
	}

	/// <summary>
	/// Shift array and cycle around from the end
	/// </summary>
	private static T[] Shift<T>(T[] array, int positions)
	{
		var copy = new T[array.Length];
		Array.Copy(array, 0, copy, array.Length - positions, positions);
		Array.Copy(array, positions, copy, 0, array.Length - positions);
		return copy;
	}

	protected string GetMonthName(int month)
	{
		return GetMonthStart(month).ToString(Culture.DateTimeFormat.YearMonthPattern, Culture);
	}

	protected abstract string GetTitleDateString();

	protected string FormatTitleDate(DateTime? date)
	{
		return date?.ToString(TitleDateFormat ?? "ddd, dd MMM", Culture) ?? "";
	}

	protected string GetFormattedYearString()
	{
		return GetMonthStart(0).ToString("yyyy", Culture);
	}

	private void OnPreviousMonthClick()
	{
		// It is impossible to go further into the past after the first year and the first month!
		if (PickerMonth.HasValue && PickerMonth.Value.Year == 1 && PickerMonth.Value.Month == 1)
		{
			return;
		}
		PickerMonth = GetMonthStart(0).AddDays(-1).StartOfMonth(Culture);
	}

	private void OnNextMonthClick()
	{
		PickerMonth = GetMonthEnd(0).AddDays(1);
	}

	private void OnPreviousYearClick()
	{
		PickerMonth = GetMonthStart(0).AddYears(-1);
	}

	private void OnNextYearClick()
	{
		PickerMonth = GetMonthStart(0).AddYears(1);
	}

	private void OnYearClick()
	{
		if (!FixYear.HasValue)
		{
			CurrentView = OpenTo.Year;
			StateHasChanged();
			_scrollToYearAfterRender = true;
		}
	}

	/// <summary>
	/// We need a random id for the year items in the year list so we can scroll to the item safely in every DatePicker.
	/// </summary>
	private string _componentId = Guid.NewGuid().ToString();

	/// <summary>
	/// Is set to true to scroll to the actual year after the next render
	/// </summary>
	private bool _scrollToYearAfterRender = false;

	public async void ScrollToYear()
	{
		_scrollToYearAfterRender = false;
		var id = $"{_componentId}{GetMonthStart(0).Year}";
		await ScrollManager.ScrollToYearAsync(id);
		StateHasChanged();
	}

	private int GetMinYear()
	{
		if (MinDate.HasValue)
			return MinDate.Value.Year;
		return DateTime.Today.Year - 100;
	}

	private int GetMaxYear()
	{
		if (MaxDate.HasValue)
			return MaxDate.Value.Year;
		return DateTime.Today.Year + 100;
	}

	private string GetYearClasses(int year)
	{
		if (year == GetMonthStart(0).Year)
			return $"picker-year-selected mud-{Color.ToDescription()}-text";
		return null;
	}

	private string GetCalendarHeaderClasses(int month)
	{
		return new CssBuilder("picker-calendar-header")
			.AddClass($"picker-calendar-header-{month + 1}")
			.AddClass($"picker-calendar-header-last", month == DisplayMonths - 1)
			.Build();
	}

	private Typo GetYearTypo(int year)
	{
		if (year == GetMonthStart(0).Year)
			return Typo.h5;
		return Typo.subtitle1;
	}

	private void OnFormattedDateClick()
	{
		// todo: raise an event the user can handle
	}


	private IEnumerable<DateTime> GetAllMonths()
	{
		var current = GetMonthStart(0);
		var calendarYear = Culture.Calendar.GetYear(current);
		var firstOfCalendarYear = Culture.Calendar.ToDateTime(calendarYear, 1, 1, 0, 0, 0, 0);
		for (var i = 0; i < Culture.Calendar.GetMonthsInYear(calendarYear); i++)
			yield return Culture.Calendar.AddMonths(firstOfCalendarYear, i);
	}

	private string GetAbbreviatedMonthName(DateTime month)
	{
		var calendarMonth = Culture.Calendar.GetMonth(month);
		return Culture.DateTimeFormat.AbbreviatedMonthNames[calendarMonth - 1];
	}

	private string GetMonthName(DateTime month)
	{
		var calendarMonth = Culture.Calendar.GetMonth(month);
		return Culture.DateTimeFormat.MonthNames[calendarMonth - 1];
	}

	private string GetMonthClasses(DateTime month)
	{
		if (GetMonthStart(0) == month)
			return $"picker-month-selected mud-{Color.ToDescription()}-text";
		return null;
	}

	private Typo GetMonthTypo(DateTime month)
	{
		if (GetMonthStart(0) == month)
			return Typo.h5;
		return Typo.subtitle1;
	}



	protected override void OnInitialized()
	{
		base.OnInitialized();
		CurrentView = OpenTo;
	}

	protected override async Task OnAfterRenderAsync(bool firstRender)
	{
		await base.OnAfterRenderAsync(firstRender);

		if (firstRender)
		{
			_picker_month ??= GetCalendarStartOfMonth();
		}

		if (firstRender && CurrentView == OpenTo.Year)
		{
			ScrollToYear();
			return;
		}

		if (_scrollToYearAfterRender)
			ScrollToYear();
	}

	protected abstract DateTime GetCalendarStartOfMonth();

	private int GetCalendarDayOfMonth(DateTime date)
	{
		return Culture.Calendar.GetDayOfMonth(date);
	}

	/// <summary>
	/// Converts gregorian year into whatever year it is in the provided culture
	/// </summary>
	/// <param name="year">Gregorian year</param>
	/// <returns>Year according to culture</returns>
	protected abstract int GetCalendarYear(int year);
}
