// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using AntDesign.Core.Extensions;
using AntDesign.Core.Documentation;
using AntDesign.Core.Extensions;
using AntDesign.Internal;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using OneOf;

namespace AntDesign
{
    public partial class RangePicker<TValue> : DatePickerBase<TValue>
    {
        private TValue _value;
        private TValue _lastValue;
        private TValue _initValue;

        /// <summary>
        /// Gets or sets the value of the input. This should be used with two-way binding.
        /// </summary>
        /// <example>
        /// @bind-Value="model.PropertyName"
        /// </example>
        [Parameter]
        public override sealed TValue Value
        {
            get { return _value; }
            set
            {
                // initial value is null, return directly
                if (value is null && _lastValue is null)
                {
                    return;
                }

                // set null, then clear the values
                if (value is null && _lastValue is not null)
                {
                    ClearValue();
                    return;
                }

                TValue orderedValue = SortValue(value);

                var hasChanged = _lastValue is null || !EqualityComparer<TValue>.Default.Equals(orderedValue, _lastValue);

                if (hasChanged)
                {
                    _value = orderedValue;

                    _lastValue ??= CreateInstance();

                    if (orderedValue is Array sourceArray && _lastValue is Array targetArray)
                    {
                        Array.Copy(sourceArray, targetArray, 2);
                    }

                    GetIfNotNull(_value, 0, (notNullValue) => PickerValues[0] = notNullValue);
                    GetIfNotNull(_value, 1, (notNullValue) => PickerValues[1] = notNullValue);

                    OnValueChange(orderedValue);
                }
            }
        }

        private readonly DateTime[] _pickerValuesAfterInit = new DateTime[2];

        /// <summary>
        /// Callback executed when range selected changes
        /// </summary>
        [Parameter]
        public EventCallback<DateRangeChangedEventArgs<TValue>> OnChange { get; set; }

        /// <summary>
        /// Add focus to picker input
        /// </summary>
        /// <param name="index">Panel index, 0 for start, 1 for end</param>
        /// <returns></returns>
        [PublicApi("1.0.0")]
        public async Task FocusAsync(int index = 0)
        {
            await base.Focus(index);
        }

        /// <summary>
        /// Remove focus from picker input
        /// </summary>
        /// <param name="index">Panel index, 0 for start, 1 for end</param>
        /// <returns></returns>
        [PublicApi("1.0.0")]
        public async Task BlurAsync(int index = 0)
        {
            await base.Blur(index);
        }

        private bool ShowFooter => !IsShowTime && (RenderExtraFooter != null || ShowRanges);

        private bool ShowRanges => Ranges is { Count: > 0 };

        private readonly Func<DateTime, bool> _defaultDisabledDateCheck;

        private Func<DateTime, bool> _disabledDate;

        [Parameter]
        public override Func<DateTime, bool> DisabledDate
        {
            get
            {
                return _disabledDate;
            }
            set
            {
                _disabledDate = (date) => (value?.Invoke(date) is true) || _defaultDisabledDateCheck(date);
            }
        }

        /// <summary>
        /// Disable the date picker. 
        /// When given a single boolean, it will disable all of it. 
        /// When given an array of booleans, it represents disabling the start/end of a range: [start, end]
        /// </summary>
        [Parameter]
        public OneOf<bool, bool[]> Disabled { get; set; } = new bool[] { false, false };

        public RangePicker()
        {
            IsRange = true;

            _defaultDisabledDateCheck = (date) =>
            {
                int? index = null;

                if (_inputEnd.IsOnFocused && GetIndexValue(0) is not null)
                {
                    index = 0;
                }
                else if (_inputStart.IsOnFocused && GetIndexValue(1) is not null)
                {
                    index = 1;
                }

                if (index is null)
                {
                    return false;
                }

                if (_pickerStatus[index.Value].SelectedValue is null)
                {
                    return false;
                }

                var value = GetIndexValue(index.Value);

                if (value is null)
                {
                    return false;
                }

                var date1 = date.Date;
                var date2 = value.Value.Date;

                if (Picker == DatePickerType.Week)
                {
                    var calendar = CultureInfo.Calendar;
                    var calendarWeekRule = CultureInfo.DateTimeFormat.CalendarWeekRule;

                    var date1Week = calendar.GetWeekOfYear(date1, calendarWeekRule, Locale.FirstDayOfWeek);
                    var date2Week = calendar.GetWeekOfYear(date2, calendarWeekRule, Locale.FirstDayOfWeek);
                    return index == 0 ? date1Week < date2Week && date1.Year <= date2.Year
                                        : date1.Year >= date2.Year && date1Week > date2Week;
                }
                else
                {
                    var formattedDate1 = DateHelper.FormatDateByPicker(date1, Picker);
                    var formattedDate2 = DateHelper.FormatDateByPicker(date2, Picker);
                    return index == 0 ? formattedDate1 < formattedDate2 : formattedDate1 > formattedDate2;
                }
            };
            DisabledDate = null;
        }

        private async Task OnInputClick(int index)
        {
            if (_duringManualInput)
            {
                return;
            }
            _openingOverlay = !_dropDown.IsOverlayShow();

            //Reset Picker to default in case the picker value was changed
            //but no value was selected (for example when a user clicks next
            //month but does not select any value)

            var currentValue = GetIndexValue(index);

            if (currentValue.HasValue)
            {
                if (index == 0 || IsShowTime)
                {
                    PickerValues[index] = currentValue.Value;
                }
                else
                {
                    var otherValue = GetIndexValue(Math.Abs(index - 1));

                    PickerValues[index] = Picker switch
                    {
                        DatePickerType.Year when DateHelper.IsSameDecade(currentValue, otherValue) => currentValue.Value,
                        DatePickerType.Week or DatePickerType.Date when DateHelper.IsSameMonth(currentValue, otherValue) => currentValue.Value,
                        DatePickerType.Quarter or DatePickerType.Month when DateHelper.IsSameYear(currentValue, otherValue) => currentValue.Value,
                        _ => GetClosingDate(currentValue.Value, -1)
                    };
                }
            }
            else if (UseDefaultPickerValue[index] && DefaultPickerValue is not null)
            {
                PickerValues[index] = InternalConvert.ToDateTimeOffset(DefaultPickerValue).Value.DateTime;
            }
            else
            {
                PickerValues[index] = _pickerValuesAfterInit[index];
            }

            await _dropDown.Show();

            if (index == 0)
            {
                // change start picker value
                if (!_inputStart.IsOnFocused && _pickerStatus[index].IsValueSelected && !UseDefaultPickerValue[index])
                {
                    GetIfNotNull(Value, index, notNullValue =>
                    {
                        ChangePickerValue(notNullValue, index);
                    });
                }

                ChangeFocusTarget(true, false);
            }
            else
            {
                // change end picker value
                if (!_inputEnd.IsOnFocused && _pickerStatus[index].IsValueSelected && !UseDefaultPickerValue[index])
                {
                    GetIfNotNull(Value, index, notNullValue =>
                    {
                        ChangePickerValue(notNullValue, index);
                    });
                }

                ChangeFocusTarget(false, true);
            }
        }

        /// <summary>
        /// Method is called via EventCallBack if the keyboard key is no longer pressed inside the Input element.
        /// </summary>
        /// <param name="e">Contains the key (combination) which was pressed inside the Input element</param>
        /// <param name="index">Refers to picker index - 0 for starting date, 1 for ending date</param>
        protected async Task OnKeyDown(KeyboardEventArgs e, int index)
        {
            if (e == null) throw new ArgumentNullException(nameof(e));

            var key = e.Key.ToUpperInvariant();

            var isEnter = key == "ENTER";
            var isTab = key == "TAB";
            var isEscape = key == "ESCAPE";
            var isOverlayShown = _dropDown.IsOverlayShow();

            if (isEnter || isTab || isEscape)
            {
                if (_duringManualInput)
                {
                    //A scenario when there are a lot of controls;
                    //It may happen that incorrect values were entered into one of the input
                    //followed by ENTER key. This event may be fired before input manages
                    //to get the value. Here we ensure that input will get that value.
                    await Task.Delay(5);
                    _duringManualInput = false;
                }
                var input = index == 0 ? _inputStart : _inputEnd;

                if (isEnter || isTab)
                {
                    if (HasTimeInput && _pickerStatus[index].SelectedValue is not null)
                    {
                        await OnOkClick();
                    }
                    else if (_pickerStatus[index].SelectedValue is not null)
                    {
                        await OnSelect(_pickerStatus[index].SelectedValue.Value, index);
                    }
                    else if (isOverlayShown)
                    {
                        if (_pickerStatus[index].SelectedValue is null && _pickerStatus[index].IsValueSelected)
                        {
                            _pickerStatus[index].SelectedValue = GetIndexValue(index);
                        }
                        if (isTab || !await SwitchFocus(index))
                        {
                            Close();

                            if (isTab && index == 1)
                            {
                                AutoFocus = false;
                            }
                        }
                    }
                    else if (!isTab)
                    {
                        await _dropDown.Show();
                    }
                }
                else if (isEscape && isOverlayShown)
                {
                    Close();
                    await Js.FocusAsync(input.Ref);
                }
            }
            else if (key == "ARROWUP")
            {
                if (isOverlayShown)
                {
                    Close();
                    AutoFocus = true;
                }
            }
            else if (!isOverlayShown)
                await _dropDown.Show();
        }

        private async Task OnFocus(int index)
        {
            if (index == 0)
            {
                if (!_inputStart.IsOnFocused)
                {
                    await Blur(1);
                    await Focus(0);
                }
            }
            else
            {
                if (!_inputEnd.IsOnFocused)
                {
                    await Blur(0);
                    await Focus(1);
                }
            }
            AutoFocus = true;
        }

        protected override async Task OnBlur(int index)
        {
            //Await for Focus event - if it is going to happen, it will be
            //right after OnBlur. Best way to achieve that is to wait.
            //Task.Yield() does not work here.
            await Task.Delay(1);

            if (ChangeOnClose && _duringManualInput)
            {
                if (_pickerStatus[index].SelectedValue is not null)
                {
                    await OnSelect(_pickerStatus[index].SelectedValue.Value, index);
                }
                else if (AllowClear)
                {
                    ClearValue(index);
                }
            }

            // if (_openingOverlay || _dropDown.IsOverlayShow())
            // {
            //     return;
            // }

            _duringManualInput = false;
            AutoFocus = false;
        }

        protected override void OnInitialized()
        {
            base.OnInitialized();
            RangePickerDefaults.ProcessDefaults(Value, DefaultValue, DefaultPickerValue, PickerValues, UseDefaultPickerValue);
            _pickerValuesAfterInit[0] = PickerValues[0];
            _pickerValuesAfterInit[1] = PickerValues[1];
            if (_value == null)
            {
                _value = CreateInstance();
                _lastValue = CreateInstance();
                ValueChanged.InvokeAsync(_value);
            }
            else
            {
                _lastValue = (TValue)(_value as Array).Clone();
            }
            ResetPlaceholder();

            _initValue = (TValue)(_value as Array).Clone();
        }

        /// <summary>
        /// Handle change of values.
        /// When values are changed, PickerValues should point to those new values
        /// or current date if no values were passed.
        /// </summary>
        /// <param name="value"></param>
        protected override void OnValueChange(TValue value)
        {
            base.OnValueChange(value);
            //reset all only if not changed using picker
            if (_inputStart?.IsOnFocused != true && _inputEnd?.IsOnFocused != true) // is null or false
            {
                UseDefaultPickerValue[0] = false;
                UseDefaultPickerValue[1] = false;
                _pickerStatus[0].IsValueSelected = true;
                _pickerStatus[1].IsValueSelected = true;
            }
        }

        /// <summary>
        /// Get value by picker index
        /// </summary>
        /// <param name="index"></param>
        /// <returns></returns>
        internal override DateTime? GetIndexValue(int index)
        {
            if (_pickerStatus[index].SelectedValue is null)
            {
                var isFocused = index == 0 && _inputStart?.IsOnFocused == true ||
                     index == 1 && _inputEnd?.IsOnFocused == true;

                DateTime? currentValue;

                if (isFocused && (currentValue = GetValue(index)) is not null
                    && _pickerStatus[Math.Abs(index - 1)].SelectedValue is not null
                    && !IsValidRange(currentValue.Value, index))
                {
                    return null;
                }
            }

            if (_pickerStatus[index].SelectedValue is not null)
            {
                return _pickerStatus[index].SelectedValue;
            }

            if (Value != null)
            {
                return GetValue(index);
            }
            else if (!IsTypedValueNull(DefaultValue, index, out var defaultValue))
            {
                return defaultValue?.DateTime;
            }
            return null;
        }

        private DateTime? GetValue(int index)
        {
            var array = Value as Array;
            var indexValue = array.GetValue(index);

            if (indexValue == null)
            {
                return null;
            }

            return InternalConvert.ToDateTime(indexValue);
        }

        private bool IsTypedValueNull(TValue value, int index, out DateTimeOffset? outValue)
        {
            var dateValue = (value as Array)?.GetValue(index);
            outValue = InternalConvert.ToDateTimeOffset(dateValue);
            return outValue == null;
        }

        internal override void ChangeValue(DateTime value, int index = 0, bool closeDropdown = true)
        {
            if (DisabledDate(value))
            {
                return;
            }

            bool isValueInstantiated = Value == null;

            if (isValueInstantiated)
            {
                Value = CreateInstance();
            }

            UseDefaultPickerValue[index] = false;

            var defaultValue = InternalConvert.ToDateTimeOffset((DefaultValue as Array)?.GetValue(index));

            var currentValueArray = Value as Array;
            var currentIndexValue = InternalConvert.ToDateTimeOffset(currentValueArray?.GetValue(index));

            var newValue = new DateTimeOffset(DateTime.SpecifyKind(FormatDateTime(value), DateTimeKind.Unspecified), defaultValue?.Offset ?? currentIndexValue?.Offset ?? DateTimeOffset.Now.Offset);

            var isValueChanged = InternalConvert.ToDateTimeOffset(currentValueArray?.GetValue(index)) != newValue;

            if (isValueChanged)
            {
                if (currentValueArray is DateTime[] dateTimeArray)
                {
                    dateTimeArray.SetValue(newValue.DateTime, index);
                }
                else if (currentValueArray is DateTime?[] nullableDateTimeArray)
                {
                    nullableDateTimeArray.SetValue(newValue.DateTime, index);
                }
                else if (currentValueArray is DateTimeOffset[] dateTimeOffsetArray)
                {
                    dateTimeOffsetArray.SetValue(newValue, index);
                }
                else if (currentValueArray is DateTimeOffset?[] nullableDateTimeOffsetArray)
                {
                    nullableDateTimeOffsetArray.SetValue(newValue, index);
                }
#if NET6_0_OR_GREATER
                else if (currentValueArray is DateOnly[] dateOnly)
                {
                    dateOnly.SetValue(DateOnly.FromDateTime(newValue.DateTime), index);
                }
                else if (currentValueArray is DateOnly?[] nullableDateOnly)
                {
                    nullableDateOnly.SetValue(DateOnly.FromDateTime(newValue.DateTime), index);
                }
                else if (currentValueArray is TimeOnly[] timeOnly)
                {
                    timeOnly.SetValue(TimeOnly.FromDateTime(newValue.DateTime), index);
                }
                else if (currentValueArray is TimeOnly?[] nullableTimeOnly)
                {
                    nullableTimeOnly.SetValue(TimeOnly.FromDateTime(newValue.DateTime), index);
                }
#endif
                else
                {
                    throw new NotImplementedException("Type not supported");
                }
            }

            var otherIndex = Math.Abs(index - 1);

            //if Value was just now instantiated then set the other index to existing DefaultValue
            if (isValueInstantiated && DefaultValue != null)
            {
                var arrayDefault = DefaultValue as Array;
                currentValueArray.SetValue(arrayDefault.GetValue(otherIndex), otherIndex);
            }

            var startDate = currentValueArray.GetValue(0);
            var endDate = currentValueArray.GetValue(1);

            if (isValueChanged && startDate is not null && endDate is not null)
            {
                var newCurrentValue = DataConversionExtensions.Convert<Array, TValue>(currentValueArray);
                _value = newCurrentValue;
                _lastValue = (TValue)(currentValueArray.Clone());
                InvokeOnChange();
                ValueChanged.InvokeAsync(newCurrentValue);
            }

            _pickerStatus[index].IsValueSelected = true;

            if (closeDropdown && !HasTimeInput
                && _pickerStatus[index].SelectedValue is not null
                && (_pickerStatus[otherIndex].SelectedValue is not null || IsDisabled(otherIndex)))
            {
                Close();
            }
        }

        internal override void ClearValue(int index = -1, bool closeDropdown = true)
        {
            _isSetPicker = false;

            var array = Value as Array;
            ReadOnlySpan<int> indexToClear;
            if (index == -1)
            {
                indexToClear = new[] { 0, 1 }; // For .NET 8+, using `ReadOnlySpan<int>` can avoid this array allocation
            }
            else
            {
                indexToClear = new[] { index };
            }

            foreach (var i in indexToClear)
            {
                if (!IsNullable && DefaultValue != null)
                {
                    var defaults = DefaultValue as Array;
                    array.SetValue(defaults.GetValue(i), i);
                }
                else
                {
                    array.SetValue(default, i);
                }

                _pickerStatus[i].SelectedValue = null;
                _pickerStatus[i].IsValueSelected = false;
                PickerValues[i] = _pickerValuesAfterInit[i];
                ResetPlaceholder(i);
            }

            if (closeDropdown)
            {
                Close();
            }

            if (array.GetValue(0) is null || array.GetValue(1) is null)
            {
                var newValue = DataConversionExtensions.Convert<Array, TValue>(array);
                _value = newValue;
                _lastValue = (TValue)(array.Clone());
                InvokeOnChange();
                ValueChanged.InvokeAsync(newValue);
            }

            OnClear.InvokeAsync(null);
            OnClearClick.InvokeAsync(null);

            _dropDown.SetShouldRender(true);
            InvokeAsync(StateHasChanged);
        }

        internal override void ResetValue()
        {
            if (CurrentValue is Array currentArray)
            {
                _isNotifyFieldChanged = false;
                (_initValue as Array).CopyTo(currentArray, 0);
                _isNotifyFieldChanged = true;
            }
            else
            {
                base.ResetValue();
            }
        }

        protected override void InvokeOnChange()
        {
            OnChange.InvokeAsync(new DateRangeChangedEventArgs<TValue>
            {
                Dates = Value,
                DateStrings = new string[] { GetInputValue(0), GetInputValue(1) }
            });
        }

        private TValue CreateInstance()
        {
            if (DefaultValue is not null)
                return (TValue)(DefaultValue as Array).Clone();

            var type = typeof(TValue);

            if (IsNullable)
            {
                if (type.IsAssignableFrom(typeof(DateTime?[])))
                {
                    return (TValue)Array.CreateInstance(typeof(DateTime?), 2).Clone();
                }
                else if (type.IsAssignableFrom(typeof(DateTimeOffset?[])))
                {
                    return (TValue)Array.CreateInstance(typeof(DateTimeOffset?), 2).Clone();
                }
#if NET6_0_OR_GREATER
                else if (type.IsAssignableFrom(typeof(DateOnly?[])))
                {
                    return (TValue)Array.CreateInstance(typeof(DateOnly?), 2).Clone();
                }
                else if (type.IsAssignableFrom(typeof(TimeOnly?[])))
                {
                    return (TValue)Array.CreateInstance(typeof(TimeOnly?), 2).Clone();
                }
#endif
                else
                {
                    throw new NotSupportedException($"{type.FullName} not supported");
                }
            }
            else
            {
                if (type.IsAssignableFrom(typeof(DateTime[])))
                {
                    return (TValue)Array.CreateInstance(typeof(DateTime), 2).Clone();
                }
                else if (type.IsAssignableFrom(typeof(DateTimeOffset[])))
                {
                    return (TValue)Array.CreateInstance(typeof(DateTimeOffset), 2).Clone();
                }
#if NET6_0_OR_GREATER
                else if (type.IsAssignableFrom(typeof(DateOnly[])))
                {
                    return (TValue)Array.CreateInstance(typeof(DateOnly), 2).Clone();
                }
                else if (type.IsAssignableFrom(typeof(TimeOnly[])))
                {
                    return (TValue)Array.CreateInstance(typeof(DateOnly), 2).Clone();
                }
#endif
                else
                {
                    throw new NotSupportedException($"{type.FullName} not supported");
                }
            }
        }

        protected override bool TryParseValueFromString(string value, out TValue result, out string validationErrorMessage)
        {
            result = default;
            validationErrorMessage = $"{FieldIdentifier.FieldName} field isn't valid.";

            if (string.IsNullOrEmpty(value))
            {
                return false;
            }

            string[] values = value.Split(',');

            if (values.Length != 2)
            {
                return false;
            }

            var success0 = BindConverter.TryConvertTo<DateTime>(values[0], CultureInfo, out var dateTime0);
            var success1 = BindConverter.TryConvertTo<DateTime>(values[1], CultureInfo, out var dateTime1);

            if (success0 && success1)
            {
                result = CreateInstance();

                var array = result as Array;

                array.SetValue(dateTime0, 0);
                array.SetValue(dateTime1, 1);

                validationErrorMessage = null;

                return true;
            }

            return false;
        }

        private async Task OverlayVisibleChange(bool isVisible)
        {
            _openingOverlay = isVisible;
            await OnOpenChange.InvokeAsync(isVisible);
            InvokeInternalOverlayVisibleChanged(isVisible);
            if (!isVisible)
            {
                // if value is changed, focus the input
                var index = GetOnFocusPickerIndex();
                await Focus(index);
            }
        }

        private async Task OnSuffixIconClick()
        {
            await Focus();
            await OnInputClick(0);
        }

        internal bool ShowClear()
        {
            return CurrentValue is Array array && (array.GetValue(0) is not null || array.GetValue(1) is not null) && AllowClear;
        }

        protected override bool IsDisabled(int? index = null)
        {
            bool disabled = false;

            Disabled.Switch(single =>
            {
                disabled = single;
            }, arr =>
            {
                if (index == null || index > 1 || index < 0)
                {
                    disabled = arr[0] && arr[1];
                }
                else
                {
                    disabled = arr[(int)index];
                }
            });

            return disabled;
        }
    }
}
