using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Diagnostics;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using MoreLinq;
using Serilog;
using Wino.Calendar.ViewModels.Data;
using Wino.Calendar.ViewModels.Interfaces;
using Wino.Calendar.ViewModels.Messages;
using Wino.Core.Domain.Collections;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar;
using Wino.Core.Domain.Models.Calendar.CalendarTypeStrategies;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.ViewModels;
using Wino.Messaging.Client.Calendar;

namespace Wino.Calendar.ViewModels;

public partial class CalendarPageViewModel : CalendarBaseViewModel,
    IRecipient<LoadCalendarMessage>,
    IRecipient<CalendarItemDeleted>,
    IRecipient<CalendarSettingsUpdatedMessage>,
    IRecipient<CalendarItemTappedMessage>,
    IRecipient<CalendarItemDoubleTappedMessage>,
    IRecipient<CalendarItemRightTappedMessage>
{
    #region Quick Event Creation

    [ObservableProperty]
    private bool _isQuickEventDialogOpen;

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(SelectedQuickEventAccountCalendarName))]
    [NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))]
    private AccountCalendarViewModel _selectedQuickEventAccountCalendar;

    public string SelectedQuickEventAccountCalendarName
    {
        get
        {
            return SelectedQuickEventAccountCalendar == null ? "Pick a calendar" : SelectedQuickEventAccountCalendar.Name;
        }
    }

    [ObservableProperty]
    private List<string> _hourSelectionStrings;

    // To be able to revert the values when the user enters an invalid time.
    private string _previousSelectedStartTimeString;
    private string _previousSelectedEndTimeString;

    [ObservableProperty]
    private DateTime? _selectedQuickEventDate;

    [ObservableProperty]
    private bool _isAllDay;

    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))]
    private string _selectedStartTimeString;

    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))]
    private string _selectedEndTimeString;

    [ObservableProperty]
    private string _location;

    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))]
    private string _eventName;

    public DateTime QuickEventStartTime => SelectedQuickEventDate.Value.Date.Add(CurrentSettings.GetTimeSpan(SelectedStartTimeString).Value);
    public DateTime QuickEventEndTime => SelectedQuickEventDate.Value.Date.Add(CurrentSettings.GetTimeSpan(SelectedEndTimeString).Value);

    public bool CanSaveQuickEvent => SelectedQuickEventAccountCalendar != null &&
                                    !string.IsNullOrWhiteSpace(EventName) &&
                                    !string.IsNullOrWhiteSpace(SelectedStartTimeString) &&
                                    !string.IsNullOrWhiteSpace(SelectedEndTimeString) &&
                                    QuickEventEndTime > QuickEventStartTime;

    #endregion

    #region Data Initialization

    [ObservableProperty]
    private CalendarOrientation _calendarOrientation = CalendarOrientation.Horizontal;

    [ObservableProperty]
    private DayRangeCollection _dayRanges = [];

    [ObservableProperty]
    private int _selectedDateRangeIndex;

    [ObservableProperty]
    private DayRangeRenderModel _selectedDayRange;

    [ObservableProperty]
    private bool _isCalendarEnabled = true;

    #endregion

    #region Event Details

    public event EventHandler DetailsShowCalendarItemChanged;

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(IsEventDetailsVisible))]
    private CalendarItemViewModel _displayDetailsCalendarItemViewModel;

    public bool IsEventDetailsVisible => DisplayDetailsCalendarItemViewModel != null;

    #endregion

    // TODO: Get rid of some of the items if we have too many.
    private const int maxDayRangeSize = 10;

    private readonly ICalendarService _calendarService;
    private readonly INavigationService _navigationService;
    private readonly IKeyPressService _keyPressService;
    private readonly IPreferencesService _preferencesService;

    // Store latest rendered options.
    private CalendarDisplayType _currentDisplayType;
    private int _displayDayCount;

    private SemaphoreSlim _calendarLoadingSemaphore = new(1);
    private bool isLoadMoreBlocked = false;

    [ObservableProperty]
    private CalendarSettings _currentSettings;

    public IStatePersistanceService StatePersistanceService { get; }
    public IAccountCalendarStateService AccountCalendarStateService { get; }

    public CalendarPageViewModel(IStatePersistanceService statePersistanceService,
                                 ICalendarService calendarService,
                                 INavigationService navigationService,
                                 IKeyPressService keyPressService,
                                 IAccountCalendarStateService accountCalendarStateService,
                                 IPreferencesService preferencesService)
    {
        StatePersistanceService = statePersistanceService;
        AccountCalendarStateService = accountCalendarStateService;

        _calendarService = calendarService;
        _navigationService = navigationService;
        _keyPressService = keyPressService;
        _preferencesService = preferencesService;

        AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested;
        AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged += AccountCalendarStateCollectivelyChanged;
    }

    private void AccountCalendarStateCollectivelyChanged(object sender, GroupedAccountCalendarViewModel e)
        => FilterActiveCalendars(DayRanges);

    private void UpdateAccountCalendarRequested(object sender, AccountCalendarViewModel e)
        => FilterActiveCalendars(DayRanges);

    private async void FilterActiveCalendars(IEnumerable<DayRangeRenderModel> dayRangeRenderModels)
    {
        await ExecuteUIThread(() =>
        {
            var days = dayRangeRenderModels.SelectMany(a => a.CalendarDays);

            days.ForEach(a => a.EventsCollection.FilterByCalendars(AccountCalendarStateService.ActiveCalendars.Select(a => a.Id)));

            DisplayDetailsCalendarItemViewModel = null;
        });
    }

    // TODO: Replace when calendar settings are updated.
    // Should be a field ideally.
    private BaseCalendarTypeDrawingStrategy GetDrawingStrategy(CalendarDisplayType displayType)
    {
        return displayType switch
        {
            CalendarDisplayType.Day => new DayCalendarDrawingStrategy(CurrentSettings),
            CalendarDisplayType.Week => new WeekCalendarDrawingStrategy(CurrentSettings),
            CalendarDisplayType.Month => new MonthCalendarDrawingStrategy(CurrentSettings),
            _ => throw new NotImplementedException(),
        };
    }

    public override void OnNavigatedFrom(NavigationMode mode, object parameters)
    {
        // Do not call base method because that will unregister messenger recipient.
        // This is a singleton view model and should not be unregistered.
    }

    public override void OnNavigatedTo(NavigationMode mode, object parameters)
    {
        base.OnNavigatedTo(mode, parameters);

        if (mode == NavigationMode.Back) return;

        RefreshSettings();

        // Automatically select the first primary calendar for quick event dialog.
        SelectedQuickEventAccountCalendar = AccountCalendarStateService.ActiveCalendars.FirstOrDefault(a => a.IsPrimary);
    }

    [RelayCommand]
    private void NavigateSeries()
    {
        if (DisplayDetailsCalendarItemViewModel == null) return;

        NavigateEvent(DisplayDetailsCalendarItemViewModel, CalendarEventTargetType.Series);
    }

    [RelayCommand]
    private void NavigateEventDetails()
    {
        if (DisplayDetailsCalendarItemViewModel == null) return;

        NavigateEvent(DisplayDetailsCalendarItemViewModel, CalendarEventTargetType.Single);
    }

    private void NavigateEvent(CalendarItemViewModel calendarItemViewModel, CalendarEventTargetType calendarEventTargetType)
    {
        var target = new CalendarItemTarget(calendarItemViewModel.CalendarItem, calendarEventTargetType);
        _navigationService.Navigate(WinoPage.EventDetailsPage, target);
    }

    [RelayCommand(AllowConcurrentExecutions = false, CanExecute = nameof(CanSaveQuickEvent))]
    private async Task SaveQuickEventAsync()
    {
        var durationSeconds = (QuickEventEndTime - QuickEventStartTime).TotalSeconds;

        var testCalendarItem = new CalendarItem
        {
            CalendarId = SelectedQuickEventAccountCalendar.Id,
            StartDate = QuickEventStartTime,
            DurationInSeconds = durationSeconds,
            CreatedAt = DateTime.UtcNow,
            Description = string.Empty,
            Location = Location,
            Title = EventName,
            Id = Guid.NewGuid()
        };

        IsQuickEventDialogOpen = false;
        await _calendarService.CreateNewCalendarItemAsync(testCalendarItem, null);

        // TODO: Create the request with the synchronizer.
    }

    [RelayCommand]
    private void MoreDetails()
    {
        // TODO: Navigate to advanced event creation  page with existing parameters.
    }

    public void SelectQuickEventTimeRange(TimeSpan startTime, TimeSpan endTime)
    {
        IsAllDay = false;

        SelectedStartTimeString = CurrentSettings.GetTimeString(startTime);
        SelectedEndTimeString = CurrentSettings.GetTimeString(endTime);
    }

    // Manage event detail popup context and select-unselect the proper items.
    // Item selection rules are defined in the selection method.
    partial void OnDisplayDetailsCalendarItemViewModelChanging(CalendarItemViewModel oldValue, CalendarItemViewModel newValue)
    {
        if (oldValue != null)
        {
            UnselectCalendarItem(oldValue);
        }

        if (newValue != null)
        {
            SelectCalendarItem(newValue);
        }
    }

    // Notify view that the detail context changed.
    // This will align the event detail popup to the selected event.
    partial void OnDisplayDetailsCalendarItemViewModelChanged(CalendarItemViewModel value)
        => DetailsShowCalendarItemChanged?.Invoke(this, EventArgs.Empty);

    private void RefreshSettings()
    {
        CurrentSettings = _preferencesService.GetCurrentCalendarSettings();

        // Populate the hour selection strings.
        var timeStrings = new List<string>();

        for (int hour = 0; hour < 24; hour++)
        {
            for (int minute = 0; minute < 60; minute += 30)
            {
                var time = new DateTime(1, 1, 1, hour, minute, 0);

                if (CurrentSettings.DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour)
                {
                    timeStrings.Add(time.ToString("HH:mm"));
                }
                else
                {
                    timeStrings.Add(time.ToString("h:mm tt"));
                }
            }
        }

        HourSelectionStrings = timeStrings;
    }

    partial void OnIsCalendarEnabledChanging(bool oldValue, bool newValue) => Messenger.Send(new CalendarEnableStatusChangedMessage(newValue));

    private bool ShouldResetDayRanges(LoadCalendarMessage message)
    {
        if (message.ForceRedraw) return true;

        // Never reset if the initiative is from the app.
        if (message.CalendarInitInitiative == CalendarInitInitiative.App) return false;

        // 1. Display type is different.
        // 2. Day display count is different.
        // 3. Display date is not in the visible range.

        if (DayRanges.DisplayRange == null) return false;

        return
            (_currentDisplayType != StatePersistanceService.CalendarDisplayType ||
            _displayDayCount != StatePersistanceService.DayDisplayCount ||
            !(message.DisplayDate >= DayRanges.DisplayRange.StartDate && message.DisplayDate <= DayRanges.DisplayRange.EndDate));
    }

    private void AdjustCalendarOrientation()
    {
        // Orientation only changes when we should reset.
        // Handle the FlipView orientation here.
        // We don't want to change the orientation while the item manipulation is going on.
        // That causes a glitch in the UI.

        bool isRequestedVerticalCalendar = StatePersistanceService.CalendarDisplayType == CalendarDisplayType.Month;
        bool isLastRenderedVerticalCalendar = _currentDisplayType == CalendarDisplayType.Month;

        if (isRequestedVerticalCalendar && !isLastRenderedVerticalCalendar)
        {
            CalendarOrientation = CalendarOrientation.Vertical;
        }
        else
        {
            CalendarOrientation = CalendarOrientation.Horizontal;
        }
    }

    public async void Receive(LoadCalendarMessage message)
    {
        await _calendarLoadingSemaphore.WaitAsync();

        try
        {
            await ExecuteUIThread(() => IsCalendarEnabled = false);

            if (ShouldResetDayRanges(message))
            {
                Debug.WriteLine("Will reset day ranges.");
                await ClearDayRangeModelsAsync();
            }
            else if (ShouldScrollToItem(message))
            {
                // Scroll to the selected date.
                Messenger.Send(new ScrollToDateMessage(message.DisplayDate));
                Debug.WriteLine("Scrolling to selected date.");
                return;
            }

            AdjustCalendarOrientation();

            // This will replace the whole collection because the user initiated a new render.
            await RenderDatesAsync(message.CalendarInitInitiative,
                                   message.DisplayDate,
                                   CalendarLoadDirection.Replace);

            // Scroll to the current hour.
            Messenger.Send(new ScrollToHourMessage(TimeSpan.FromHours(DateTime.Now.Hour)));
        }
        catch (Exception ex)
        {
            Log.Error(ex, "Error while loading calendar.");
            Debugger.Break();
        }
        finally
        {
            _calendarLoadingSemaphore.Release();

            await ExecuteUIThread(() => IsCalendarEnabled = true);
        }
    }


    private async Task AddDayRangeModelAsync(DayRangeRenderModel dayRangeRenderModel)
    {
        if (dayRangeRenderModel == null) return;

        await ExecuteUIThread(() =>
        {
            DayRanges.Add(dayRangeRenderModel);
        });
    }

    private async Task InsertDayRangeModelAsync(DayRangeRenderModel dayRangeRenderModel, int index)
    {
        if (dayRangeRenderModel == null) return;

        await ExecuteUIThread(() =>
        {
            DayRanges.Insert(index, dayRangeRenderModel);
        });
    }

    private async Task RemoveDayRangeModelAsync(DayRangeRenderModel dayRangeRenderModel)
    {
        if (dayRangeRenderModel == null) return;

        await ExecuteUIThread(() =>
        {
            DayRanges.Remove(dayRangeRenderModel);
        });
    }

    private async Task ClearDayRangeModelsAsync()
    {
        await ExecuteUIThread(() =>
        {
            DayRanges.Clear();
        });
    }

    private async Task RenderDatesAsync(CalendarInitInitiative calendarInitInitiative,
                                        DateTime? loadingDisplayDate = null,
                                        CalendarLoadDirection calendarLoadDirection = CalendarLoadDirection.Replace)
    {
        isLoadMoreBlocked = calendarLoadDirection == CalendarLoadDirection.Replace;

        // This is the part we arrange the flip view calendar logic.

        /* Loading for a month of the selected date is fine.
         * If the selected date is in the loaded range, we'll just change the selected flip index to scroll.
         * If the selected date is not in the loaded range:
         * 1. Detect the direction of the scroll.
         * 2. Load the next month.
         * 3. Replace existing month with the new month.
         */

        // 2 things are important: How many items should 1 flip have, and, where we should start loading?

        // User initiated renders must always have a date to start with.
        if (calendarInitInitiative == CalendarInitInitiative.User) Guard.IsNotNull(loadingDisplayDate, nameof(loadingDisplayDate));

        var strategy = GetDrawingStrategy(StatePersistanceService.CalendarDisplayType);
        var displayDate = loadingDisplayDate.GetValueOrDefault();

        // How many days should be placed in 1 flip view item?
        int eachFlipItemCount = strategy.GetRenderDayCount(displayDate, StatePersistanceService.DayDisplayCount);

        DateRange flipLoadRange = null;


        if (calendarInitInitiative == CalendarInitInitiative.User || DayRanges.DisplayRange == null)
        {
            flipLoadRange = strategy.GetRenderDateRange(displayDate, StatePersistanceService.DayDisplayCount);
        }
        else
        {
            // App is trying to load.
            // This should be based on direction. We'll load the next or previous range.
            // DisplayDate is either the start or end date of the current visible range.

            if (calendarLoadDirection == CalendarLoadDirection.Previous)
            {
                flipLoadRange = strategy.GetPreviousDateRange(DayRanges.DisplayRange, StatePersistanceService.DayDisplayCount);
            }
            else
            {
                flipLoadRange = strategy.GetNextDateRange(DayRanges.DisplayRange, StatePersistanceService.DayDisplayCount);
            }
        }

        // Create day ranges for each flip item until we reach the total days to load.
        int totalFlipItemCount = (int)Math.Ceiling((double)flipLoadRange.TotalDays / eachFlipItemCount);

        List<DayRangeRenderModel> renderModels = new();

        for (int i = 0; i < totalFlipItemCount; i++)
        {
            var startDate = flipLoadRange.StartDate.AddDays(i * eachFlipItemCount);
            var endDate = startDate.AddDays(eachFlipItemCount);

            var range = new DateRange(startDate, endDate);
            var renderOptions = new CalendarRenderOptions(range, CurrentSettings);

            var dayRangeHeaderModel = new DayRangeRenderModel(renderOptions);
            renderModels.Add(dayRangeHeaderModel);
        }

        // Dates are loaded. Now load the events for them.
        foreach (var renderModel in renderModels)
        {
            await InitializeCalendarEventsForDayRangeAsync(renderModel).ConfigureAwait(false);
        }

        // Filter by active calendars. This is a quick operation, and things are not on the UI yet.
        FilterActiveCalendars(renderModels);

        CalendarLoadDirection animationDirection = calendarLoadDirection;

        //bool removeCurrent = calendarLoadDirection == CalendarLoadDirection.Replace;

        if (calendarLoadDirection == CalendarLoadDirection.Replace)
        {
            // New date ranges are being replaced.
            // We must preserve existing selection if any, add the items before/after the current one, remove the current one.
            // This will make sure the new dates are animated in the correct direction.

            isLoadMoreBlocked = true;

            // Remove all other dates except this one.
            var rangesToRemove = DayRanges.Where(a => a != SelectedDayRange).ToList();

            foreach (var range in rangesToRemove)
            {
                await RemoveDayRangeModelAsync(range);
            }

            animationDirection = displayDate <= SelectedDayRange?.CalendarRenderOptions.DateRange.StartDate ?
                CalendarLoadDirection.Previous : CalendarLoadDirection.Next;
        }

        if (animationDirection == CalendarLoadDirection.Next)
        {
            foreach (var item in renderModels)
            {
                await AddDayRangeModelAsync(item);
            }
        }
        else if (animationDirection == CalendarLoadDirection.Previous)
        {
            // Wait for the animation to finish.
            // Otherwise it somehow shutters a little, which is annoying.

            // if (!removeCurrent) await Task.Delay(350);

            // Insert each render model in reverse order.
            for (int i = renderModels.Count - 1; i >= 0; i--)
            {
                await InsertDayRangeModelAsync(renderModels[i], 0);
            }
        }

        Debug.WriteLine($"Flip count: ({DayRanges.Count})");

        foreach (var item in DayRanges)
        {
            Debug.WriteLine($"- {item.CalendarRenderOptions.DateRange.ToString()}");
        }

        //if (removeCurrent)
        //{
        //    await RemoveDayRangeModelAsync(SelectedDayRange);
        //}

        // TODO...
        // await TryConsolidateItemsAsync();

        isLoadMoreBlocked = false;

        // Only scroll if the render is initiated by user.
        // Otherwise we'll scroll to the app rendered invisible date range.
        if (calendarInitInitiative == CalendarInitInitiative.User)
        {
            // Save the current settings for the page for later comparison.
            _currentDisplayType = StatePersistanceService.CalendarDisplayType;
            _displayDayCount = StatePersistanceService.DayDisplayCount;

            Messenger.Send(new ScrollToDateMessage(displayDate));
        }
    }

    protected override async void OnCalendarItemAdded(CalendarItem calendarItem)
    {
        base.OnCalendarItemAdded(calendarItem);

        // Check if event falls into the current date range.


        if (DayRanges.DisplayRange == null) return;

        // Check whether this event falls into any of the loaded date ranges.
        var allDaysForEvent = DayRanges.SelectMany(a => a.CalendarDays).Where(a => a.Period.OverlapsWith(calendarItem.Period));

        foreach (var calendarDay in allDaysForEvent)
        {
            var calendarItemViewModel = new CalendarItemViewModel(calendarItem);

            await ExecuteUIThread(() =>
            {
                calendarDay.EventsCollection.AddCalendarItem(calendarItemViewModel);
            });
        }

        FilterActiveCalendars(DayRanges);
    }

    private async Task InitializeCalendarEventsForDayRangeAsync(DayRangeRenderModel dayRangeRenderModel)
    {
        // Clear all events first for all days.
        foreach (var day in dayRangeRenderModel.CalendarDays)
        {
            await ExecuteUIThread(() =>
            {
                day.EventsCollection.Clear();
            });
        }

        // Initialization is done for all calendars, regardless whether they are actively selected or not.
        // This is because the filtering is cached internally of the calendar items in CalendarEventCollection.
        var allCalendars = AccountCalendarStateService.GroupedAccountCalendars.SelectMany(a => a.AccountCalendars);

        foreach (var calendarViewModel in allCalendars)
        {
            // Check all the events for the given date range and calendar.
            // Then find the day representation for all the events returned, and add to the collection.

            var events = await _calendarService.GetCalendarEventsAsync(calendarViewModel, dayRangeRenderModel).ConfigureAwait(false);

            foreach (var @event in events)
            {
                // Find the days that the event falls into.
                var allDaysForEvent = dayRangeRenderModel.CalendarDays.Where(a => a.Period.OverlapsWith(@event.Period));

                foreach (var calendarDay in allDaysForEvent)
                {
                    var calendarItemViewModel = new CalendarItemViewModel(@event);
                    await ExecuteUIThread(() =>
                    {
                        calendarDay.EventsCollection.AddCalendarItem(calendarItemViewModel);
                    });
                }
            }
        }
    }

    private async Task TryConsolidateItemsAsync()
    {
        // Check if trimming is necessary
        if (DayRanges.Count > maxDayRangeSize)
        {
            Debug.WriteLine("Trimming items.");

            isLoadMoreBlocked = true;

            var removeCount = DayRanges.Count - maxDayRangeSize;

            await Task.Delay(500);

            // Right shifted, remove from the start.
            if (SelectedDateRangeIndex > DayRanges.Count / 2)
            {
                DayRanges.RemoveRange(DayRanges.Take(removeCount).ToList());
            }
            else
            {
                // Left shifted, remove from the end.
                DayRanges.RemoveRange(DayRanges.Skip(DayRanges.Count - removeCount).Take(removeCount));
            }

            SelectedDateRangeIndex = DayRanges.IndexOf(SelectedDayRange);
        }
    }

    private bool ShouldScrollToItem(LoadCalendarMessage message)
    {
        // Never scroll if the initiative is from the app.
        if (message.CalendarInitInitiative == CalendarInitInitiative.App) return false;

        // Nothing to scroll.
        if (DayRanges.Count == 0) return false;

        if (DayRanges.DisplayRange == null) return false;

        var selectedDate = message.DisplayDate;

        return selectedDate >= DayRanges.DisplayRange.StartDate && selectedDate <= DayRanges.DisplayRange.EndDate;
    }

    partial void OnIsAllDayChanged(bool value)
    {
        if (value)
        {
            SelectedStartTimeString = HourSelectionStrings.FirstOrDefault();
            SelectedEndTimeString = HourSelectionStrings.FirstOrDefault();
        }
        else
        {
            SelectedStartTimeString = _previousSelectedStartTimeString;
            SelectedEndTimeString = _previousSelectedEndTimeString;
        }
    }

    partial void OnSelectedStartTimeStringChanged(string newValue)
    {
        var parsedTime = CurrentSettings.GetTimeSpan(newValue);

        if (parsedTime == null)
        {
            SelectedStartTimeString = _previousSelectedStartTimeString;
        }
        else if (IsAllDay)
        {
            _previousSelectedStartTimeString = newValue;
        }
    }

    partial void OnSelectedEndTimeStringChanged(string newValue)
    {
        var parsedTime = CurrentSettings.GetTimeSpan(newValue);

        if (parsedTime == null)
        {
            SelectedEndTimeString = _previousSelectedStartTimeString;
        }
        else if (IsAllDay)
        {
            _previousSelectedEndTimeString = newValue;
        }
    }

    partial void OnSelectedDayRangeChanged(DayRangeRenderModel value)
    {
        DisplayDetailsCalendarItemViewModel = null;

        if (DayRanges.Count == 0 || SelectedDateRangeIndex < 0) return;

        var selectedRange = DayRanges[SelectedDateRangeIndex];

        Messenger.Send(new VisibleDateRangeChangedMessage(new DateRange(selectedRange.Period.Start, selectedRange.Period.End)));

        if (isLoadMoreBlocked) return;

        _ = LoadMoreAsync();
    }

    private async Task LoadMoreAsync()
    {
        try
        {
            await _calendarLoadingSemaphore.WaitAsync();

            // Depending on the selected index, we'll load more dates.
            // Day ranges may change while the async update is in progress.
            // Therefore we wait for semaphore to be released before we continue.
            // There is no need to load more if the current index is not in ideal position.

            if (SelectedDateRangeIndex == 0)
            {
                await RenderDatesAsync(CalendarInitInitiative.App, calendarLoadDirection: CalendarLoadDirection.Previous);
            }
            else if (SelectedDateRangeIndex == DayRanges.Count - 1)
            {
                await RenderDatesAsync(CalendarInitInitiative.App, calendarLoadDirection: CalendarLoadDirection.Next);
            }
        }
        catch (Exception)
        {
            Debugger.Break();
        }
        finally
        {
            _calendarLoadingSemaphore.Release();
        }
    }

    public void Receive(CalendarSettingsUpdatedMessage message)
    {
        RefreshSettings();

        // TODO: This might need throttling due to slider in the settings page for hour height.
        // or make sure the slider does not update on each tick but on focus lost.

        // Messenger.Send(new LoadCalendarMessage(DateTime.UtcNow.Date, CalendarInitInitiative.App, true));
    }

    private IEnumerable<CalendarItemViewModel> GetCalendarItems(CalendarItemViewModel calendarItemViewModel, CalendarDayModel selectedDay)
    {
        // All-day and multi-day events are selected collectively.
        // Recurring events must be selected as a single instance.
        // We need to find the day that the event is in, and then select the event.

        if (!calendarItemViewModel.IsRecurringEvent)
        {
            return [calendarItemViewModel];
        }
        else
        {
            return DayRanges
                .SelectMany(a => a.CalendarDays)
                .Select(b => b.EventsCollection.GetCalendarItem(calendarItemViewModel.Id))
                .Where(c => c != null)
                .Cast<CalendarItemViewModel>()
                .Distinct();
        }
    }

    private void UnselectCalendarItem(CalendarItemViewModel calendarItemViewModel, CalendarDayModel calendarDay = null)
    {
        if (calendarItemViewModel == null) return;

        var itemsToUnselect = GetCalendarItems(calendarItemViewModel, calendarDay);

        foreach (var item in itemsToUnselect)
        {
            item.IsSelected = false;
        }
    }

    private void SelectCalendarItem(CalendarItemViewModel calendarItemViewModel, CalendarDayModel calendarDay = null)
    {
        if (calendarItemViewModel == null) return;

        var itemsToSelect = GetCalendarItems(calendarItemViewModel, calendarDay);

        foreach (var item in itemsToSelect)
        {
            item.IsSelected = true;
        }
    }

    public void Receive(CalendarItemTappedMessage message)
    {
        if (message.CalendarItemViewModel == null) return;

        DisplayDetailsCalendarItemViewModel = message.CalendarItemViewModel;
    }

    public void Receive(CalendarItemDoubleTappedMessage message) => NavigateEvent(message.CalendarItemViewModel, CalendarEventTargetType.Single);

    public void Receive(CalendarItemRightTappedMessage message)
    {

    }

    public async void Receive(CalendarItemDeleted message)
    {
        // Each deleted recurrence will report for it's own.

        await ExecuteUIThread(() =>
        {
            var deletedItem = message.CalendarItem;

            // Event might be spreaded into multiple days.
            // Remove from all.

            // var calendarItems = GetCalendarItems(deletedItem.Id);
        });
    }
}
