@using Radzen
@using Radzen.Blazor
@using Radzen.Blazor.Rendering
@using Microsoft.JSInterop
@inject DialogService DialogService
@inject IJSRuntime JSRuntime
@inherits DropableViewBase
@implements IAsyncDisposable


<div class="rz-view rz-month-view">
    <div class="rz-view-header">
        @for (var date = StartDate; date <= StartDate.EndOfWeek(Scheduler?.Culture ?? System.Globalization.CultureInfo.CurrentCulture); date = date.AddDays(1))
        {
            <div class="rz-slot-header">
                @date.ToString("ddd", Scheduler?.Culture ?? System.Globalization.CultureInfo.CurrentCulture)
            </div>
        }
    </div>
    @{
        var days = 0;
    }
    <div @ref=contentView class="rz-view-content" tabindex="0" @onkeydown="@(args => OnKeyPress(args))" @onkeydown:preventDefault="@preventKeyPress" @onkeydown:stopPropagation="stopKeydownPropagation"
         @onfocus=@this.AsNonRenderingEventHandler(OnFocus)>
         @foreach (var week in (weeks ?? new List<Week>()))
         {
            if (week?.Bands == null || week?.Days == null) continue;
            <div class="rz-week">
                <div class="rz-events">
                    @for (var bandIndex = 0; bandIndex < week.Bands.Length; bandIndex++)
                    {
                        var band = week.Bands[bandIndex];

                        foreach (var info in band)
                        {
                            var dayIndex = info.DayIndex;
                            var day = week.Days[dayIndex];
                            var slotWidth = 100 / 7.0;
                            var left = dayIndex * slotWidth;
                            var length = Math.Max(1, Math.Ceiling(info!.Data!.End.Subtract(week.Start).TotalDays) - dayIndex);
                            var width = info.Data.End <= week.End ? (length) * slotWidth : (7 - dayIndex) * slotWidth;
                            var top = (bandIndex + 1) * 1.5;
                            var height = 1.5;

                            <Appointment Data=@info.Data Top=@top Left=@left Width=@width Height=@height Click=@OnAppointmentClick
                                CssClass="@(CurrentDate >= day.Start && CurrentDate <= day.Start.AddDays(1) && object.Equals(currentAppointments?.ElementAtOrDefault(currentAppointment), info.Data) ? "rz-state-focused" : "")"
                                DragStart=@OnAppointmentDragStart />
                        }
                    }
                </div>
                <div class="rz-slots">
                @for (var day = 0; day < 7; day ++)
                {
                    var dayOfWeek = StartDate.AddDays(days++);
                    var slot = week.Days[day];
                    <div @onclick="@(args => OnSlotClick(dayOfWeek))" @ondragover:preventDefault @ondrop=@(args => @OnDrop(dayOfWeek)) @attributes=@Attributes(dayOfWeek)>
                        <div class="rz-slot-title @(dayOfWeek == CurrentDate ? " rz-state-focused" : "")" role="button" tabindex="0"
                             @onclick=@(args => OnDayClick(dayOfWeek)) @onclick:stopPropagation
                             @onkeydown="@(args => OnDayHeaderKeyDown(args, dayOfWeek))">
                            @((Scheduler?.Culture ?? System.Globalization.CultureInfo.CurrentCulture).Calendar.GetDayOfMonth(dayOfWeek))
                            @if (slot.Hidden > 0)
                            {
                                <button type="button" class="rz-event-list-btn" @onclick=@(args => { if (slot.Appointments != null && MoreText != null) _ = OnListClick(slot.Start, slot.Appointments); }) @onclick:stopPropagation>@(MoreText != null ? String.Format(MoreText, slot.Hidden) : "")</button>
                            }
                        </div>
                    </div>
                }
                </div>
            </div>
         }
    </div>
</div>

@code {
    class Day
    {
        public AppointmentData[]? Appointments { get; set; }
        public DateTime Start { get; set; }
        public DateTime End { get; set; }
        public int Hidden { get; set; }
    }

    class AppointmentRenderInfo
    {
        public AppointmentData? Data { get; set; }
        public int DayIndex { get; set; }
    }

    class Week
    {
        public DateTime Start { get; set; }
        public DateTime End { get; set; }
        public IList<AppointmentRenderInfo>[]? Bands { get; set; }
        public Day[]? Days { get; set; }
    }

    Week CreateWeek(DateTime weekStart, DateTime weekEnd)
    {
        var bands = Enumerable.Range(0, EffectiveMaxAppointmentsInSlot).Select(_ => new List<AppointmentRenderInfo>()).ToArray();

        var days = new List<Day>();
        var start = weekStart;
        var dayIndex = 0;

        while (start < weekEnd)
        {
            var end = start.AddDays(1);

            var appointments = AppointmentsInSlot(start, end);

            var slot = new Day { Start = start, End = end, Appointments = appointments };
            dayIndex = days.Count;
            days.Add(slot);

            foreach (var appointment in appointments)
            {
                var placed = bands.Any(b => b.Any(a => a.Data != null && a.Data.Equals(appointment)));

                if (placed)
                {
                    continue;
                }

                foreach (var band in bands)
                {
                    if (band.Count == 0)
                    {
                        band.Add(new AppointmentRenderInfo { Data = appointment, DayIndex = dayIndex });

                        placed = true;

                        break;
                    }

                    var lastAppointmentInBand = band[^1];
                    if (lastAppointmentInBand.Data == null)
                    {
                        continue;
                    }
                    var lastStart = days[lastAppointmentInBand.DayIndex].Start;
                    var lastEnd = lastAppointmentInBand.Data.End;

                    if (lastStart == lastEnd && lastEnd == slot.Start)
                    {
                        continue;
                    }

                    // Check if the appointment overlaps with the last appointment in the band
                    if (lastStart < appointment.End && lastEnd > slot.Start)
                    {
                        // Overlap, we cannot place the appointment in this band
                        continue;
                    }

                    if (lastStart == appointment.End && lastEnd == slot.Start)
                    {
                        // Overlap, we cannot place the appointment in this band
                        continue;
                    }
                    
                    if(lastAppointmentInBand.Data.Start < slot.Start && lastAppointmentInBand.Data.End > slot.End)
                    {
                        // Overlap, we cannot place the appointment in this band
                        continue;
					}

                    // No overlap, we can add the appointment to this band if there is space for it. A band can contain appointments whose total duration is less than one week.
                    var bandDuration = band.Sum(a => a.Data != null ? a.Data.End.Subtract(days[a.DayIndex]?.Start ?? DateTime.MinValue).TotalDays : 0);

                    if (bandDuration < 7)
                    {
                        band.Add(new AppointmentRenderInfo { Data = appointment, DayIndex = dayIndex });

                        placed = true;

                        break;
                    }
                    else
                    {
                        // No space in this band, try the next one
                        continue;
                    }
                }

                if (!placed)
                {
                    slot.Hidden++;
                }
            }

            start = end;
        }

        return new Week { Start = weekStart, End = weekEnd, Bands = bands.ToArray(), Days = days.ToArray() };
    }

    void OnFocus()
    {
        if (CurrentDate == default(DateTime)) 
        { 
            CurrentDate = StartDate; 
        }
    }

    [Parameter]
    public DateTime StartDate { get; set; }

    [Parameter]
    public DateTime EndDate { get; set; }

    [Parameter]
    public int? MaxAppointmentsInSlot { get; set; }

    private double contentHeight;
    private DotNetObjectReference<MonthView>? reference;

    private int EffectiveMaxAppointmentsInSlot
    {
        get
        {
            if (MaxAppointmentsInSlot != null)
            {
                return MaxAppointmentsInSlot.Value;
            }

            var numberOfWeeks = (int)Math.Max(1, Math.Ceiling((EndDate - StartDate).TotalDays / 7));

            if (contentHeight > 0)
            {
                var slotHeight = contentHeight / numberOfWeeks;
                return Math.Max(0, Convert.ToInt32(Math.Floor(slotHeight / 24)) - 1);
            }

            return 0;
        }
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            reference = DotNetObjectReference.Create(this);
            var rect = await JSRuntime.InvokeAsync<Rect>("Radzen.createResizable", contentView, reference);
            if (rect.Height > 0 && MaxAppointmentsInSlot == null)
            {
                contentHeight = rect.Height;
                Refresh();
                StateHasChanged();
            }
        }
    }

    [JSInvokable]
    public void Resize(double width, double height)
    {
        if (MaxAppointmentsInSlot != null)
        {
            return;
        }

        if (contentHeight != height)
        {
            contentHeight = height;
            Refresh();
            StateHasChanged();
        }
    }

    public async ValueTask DisposeAsync()
    {
        try
        {
            await JSRuntime.InvokeVoidAsync("Radzen.destroyResizable", contentView);
        }
        catch
        {
        }

        reference?.Dispose();
    }

    [Parameter]
    public string? MoreText { get; set; }

    [CascadingParameter]
    public IScheduler? Scheduler { get; set; }

    [Parameter]
    public IEnumerable<AppointmentData>? Appointments { get; set; }

    IDictionary<string, object> Attributes(DateTime start)
    {
        if (Scheduler == null)
            return new Dictionary<string, object>();
        var attributes = Scheduler.GetSlotAttributes(start, start.AddDays(1), () => AppointmentsInSlot(start, start.AddDays(1)));
        attributes["class"] = ClassList.Create("rz-slot").Add(attributes).ToString();
        attributes["dropzone"] = "move";
        return attributes;
    }

    async Task OnSlotClick(DateTime date)
    {
        if (Scheduler != null)
            await Scheduler.SelectSlot(date, date.AddDays(1), AppointmentsInSlot(date, date.AddDays(1)));
    }

    async Task OnDayClick(DateTime day)
    {
        if (Scheduler != null)
            await Scheduler.SelectDay(day, AppointmentsInSlot(day, day.AddDays(1)));
    }

    async Task OnDayHeaderKeyDown(KeyboardEventArgs args, DateTime day)
    {
        var key = args.Code != null ? args.Code : args.Key;
        if (key == "Enter" || key == "Space")
        {
            await OnDayClick(day);
        }
    }

    ElementReference contentView;

    async Task OnAppointmentClick(AppointmentData data)
    {
        if (Scheduler != null)
        {
            await Scheduler.SelectAppointment(data);

            try
            {
                await contentView.FocusAsync();
            }
            catch (JSException) {}
        }
    }

    private AppointmentData[] AppointmentsInSlot(DateTime start, DateTime end)
    {
        if (Appointments == null || Scheduler == null)
        {
            return Array.Empty<AppointmentData>();
        }

        return Appointments.Where(item => Scheduler.IsAppointmentInRange(item, start, end))
                .OrderByDescending(item => item.End.Subtract(item.Start).TotalDays > 1)
                .ThenBy(item => item.Start)
                .ThenByDescending(item => item.End)
                .ToArray();
    }

    async Task OnListClick(DateTime date, IEnumerable<AppointmentData> appointments)
    {
        if (Scheduler == null) return;
        bool preventDefault = await Scheduler.SelectMore(date, date.AddDays(1), appointments);

        if (!preventDefault)
        {
            await DialogService.OpenAsync(date.ToString("d", Scheduler.Culture), ds =>
                @<div class="rz-event-list">
                    <CascadingValue Value=@Scheduler>
                        @foreach(var item in appointments)
                        {
                            <Appointment Data=@item Click="OnListEventClick" />
                        }
                    </CascadingValue>
                </div>
            );
        }
    }

    private List<Week>? weeks;

    void Refresh()
    {
        weeks = new List<Week>();

        for (var date = StartDate; date < EndDate; date = date.AddDays(7))
        {
            weeks.Add(CreateWeek(date, date.AddDays(7)));
        }
    }

    public override async Task SetParametersAsync(ParameterView parameters)
    {
        var didAppointmentsChange = parameters.DidParameterChange(nameof(Appointments), Appointments);
        var didStartDateChange = parameters.DidParameterChange(nameof(StartDate), StartDate);
        var didEndDateChange = parameters.DidParameterChange(nameof(EndDate), EndDate);
        var didMaxAppointmentsInSlotChange = parameters.DidParameterChange(nameof(MaxAppointmentsInSlot), MaxAppointmentsInSlot);

        await base.SetParametersAsync(parameters);

        if (didAppointmentsChange || didStartDateChange || didEndDateChange || didMaxAppointmentsInSlotChange)
        {
            Refresh();
        }
    }

    async Task OnListEventClick(AppointmentData data)
    {
        DialogService.Close();

        await OnAppointmentClick(data);
    }

    DateTime CurrentDate;
    int currentAppointment = -1;
    IEnumerable<AppointmentData> currentAppointments = Enumerable.Empty<AppointmentData>();
    bool preventKeyPress;
    bool stopKeydownPropagation;

    async Task OnKeyPress(KeyboardEventArgs args)
    {
        var key = args.Code != null ? args.Code : args.Key;
        var arrow = key == "ArrowLeft" || key == "ArrowRight" || key == "ArrowUp" || key == "ArrowDown";
        var updown = key == "ArrowUp" || key == "ArrowDown";
        var leftright = key == "ArrowLeft" || key == "ArrowRight";
        var leftup = key == "ArrowLeft" || key == "ArrowUp";

        if (arrow && !currentAppointments.Any())
        {
            var days = leftright ? 1 : 7;
            CurrentDate = CurrentDate.AddDays(leftup ? -days : days);
            currentAppointments = AppointmentsInSlot(CurrentDate, CurrentDate.AddDays(1)).Take(EffectiveMaxAppointmentsInSlot);

            preventKeyPress = true;
            stopKeydownPropagation = true;
        }
        else if (arrow && currentAppointments.Any())
        {
            if (updown)
            {
                currentAppointment = Math.Clamp(currentAppointment + (key == "ArrowUp" ? -1 : 1), 0, currentAppointments.Count() - 1);
            }
            else
            {
                CurrentDate = CurrentDate.AddDays(key == "ArrowLeft" ? -1 : 1);
                currentAppointments = AppointmentsInSlot(CurrentDate, CurrentDate.AddDays(1)).Take(EffectiveMaxAppointmentsInSlot);
            }

            preventKeyPress = true;
            stopKeydownPropagation = true;
        }
        else if (key == "Enter")
        {
            await OnSlotClick(CurrentDate);
            await contentView.FocusAsync();

            preventKeyPress = true;
            stopKeydownPropagation = true;
        }
        else if (key == "Space")
        {
            var appointment = currentAppointments.ElementAtOrDefault(currentAppointment);
            if (appointment != null)
            {
                await OnAppointmentClick(appointment);
            }

            preventKeyPress = true;
            stopKeydownPropagation = true;
        }
        else
        {
            preventKeyPress = false;
            stopKeydownPropagation = false;
        }
    }
}