using System;
using Xamarin.Forms;

namespace Billing.UI
{
    public partial class BillingDate : ContentView
    {
        #region UI Properties

        private static readonly BindableProperty SundayProperty = BindableProperty.Create(nameof(Sunday), typeof(BillingDay), typeof(BillingDate));
        private static readonly BindableProperty MondayProperty = BindableProperty.Create(nameof(Monday), typeof(BillingDay), typeof(BillingDate));
        private static readonly BindableProperty TuesdayProperty = BindableProperty.Create(nameof(Tuesday), typeof(BillingDay), typeof(BillingDate));
        private static readonly BindableProperty WednesdayProperty = BindableProperty.Create(nameof(Wednesday), typeof(BillingDay), typeof(BillingDate));
        private static readonly BindableProperty ThursdayProperty = BindableProperty.Create(nameof(Thursday), typeof(BillingDay), typeof(BillingDate));
        private static readonly BindableProperty FridayProperty = BindableProperty.Create(nameof(Friday), typeof(BillingDay), typeof(BillingDate));
        private static readonly BindableProperty SaturdayProperty = BindableProperty.Create(nameof(Saturday), typeof(BillingDay), typeof(BillingDate));

        public BillingDay Sunday => (BillingDay)GetValue(SundayProperty);
        public BillingDay Monday => (BillingDay)GetValue(MondayProperty);
        public BillingDay Tuesday => (BillingDay)GetValue(TuesdayProperty);
        public BillingDay Wednesday => (BillingDay)GetValue(WednesdayProperty);
        public BillingDay Thursday => (BillingDay)GetValue(ThursdayProperty);
        public BillingDay Friday => (BillingDay)GetValue(FridayProperty);
        public BillingDay Saturday => (BillingDay)GetValue(SaturdayProperty);

        #endregion

        private static BindableProperty GetWeekProperty(int week)
        {
            return (DayOfWeek)week switch
            {
                DayOfWeek.Monday => MondayProperty,
                DayOfWeek.Tuesday => TuesdayProperty,
                DayOfWeek.Wednesday => WednesdayProperty,
                DayOfWeek.Thursday => ThursdayProperty,
                DayOfWeek.Friday => FridayProperty,
                DayOfWeek.Saturday => SaturdayProperty,
                _ => SundayProperty
            };
        }

        public static readonly BindableProperty LocatedDateProperty = BindableProperty.Create(nameof(LocatedDate), typeof(DateTime), typeof(BillingDate), propertyChanged: OnLocatedDatePropertyChanged);
        public static readonly BindableProperty SelectedDateProperty = BindableProperty.Create(nameof(SelectedDate), typeof(DateTime), typeof(BillingDate), propertyChanged: OnSelectedDatePropertyChanged);

        private static void OnLocatedDatePropertyChanged(BindableObject obj, object old, object @new)
        {
            if (obj is BillingDate billingDate && @new is DateTime date)
            {
                var week = (int)date.DayOfWeek;
                var tmpDate = date.AddDays(-week);
                for (var i = 0; i < 7; i++)
                {
                    var prop = GetWeekProperty(i);
                    var day = new BillingDay(tmpDate);
                    billingDate.SetValue(prop, day);
                    tmpDate = tmpDate.AddDays(1);
                }
            }
        }

        private static void OnSelectedDatePropertyChanged(BindableObject obj, object old, object @new)
        {
            if (obj is BillingDate billingDate && @new is DateTime selected)
            {
                for (var i = 0; i < 7; i++)
                {
                    var prop = GetWeekProperty(i);
                    var day = (BillingDay)billingDate.GetValue(prop);
                    day.Refresh(selected);
                }
            }
        }

        public DateTime LocatedDate
        {
            get => (DateTime)GetValue(LocatedDateProperty);
            set => SetValue(LocatedDateProperty, value);
        }
        public DateTime SelectedDate
        {
            get => (DateTime)GetValue(SelectedDateProperty);
            set => SetValue(SelectedDateProperty, value);
        }

        public Command OnDayTapped { get; }

        public event EventHandler<DateEventArgs> DateSelected;

        private DateTime lastDate;
        private double? x1;
        private double x2;

        public BillingDate()
        {
            OnDayTapped = new Command(OnDayChanged);

            InitializeComponent();
        }

        public void SetDateTime(DateTime selectedDate, DateTime? locatedDate = null)
        {
            if (locatedDate != null)
            {
                LocatedDate = locatedDate.Value;
            }
            else
            {
                LocatedDate = selectedDate;
            }
            SelectedDate = selectedDate;
            DateSelected?.Invoke(this, new DateEventArgs(selectedDate));
        }

        private void OnDayChanged(object o)
        {
            var selected = SelectedDate;
            if (o is BillingDay day && (selected.Year != day.Date.Year || selected.DayOfYear != day.Date.DayOfYear))
            {
                for (var i = 0; i < 7; i++)
                {
                    var d = (BillingDay)GetValue(GetWeekProperty(i));
                    if (d.IsSelected)
                    {
                        this.AbortAnimation("unselected");
                        this.Animate("unselected", v =>
                        {
                            d.Opacity = v;
                        },
                        start: 1, end: 0,
                        easing: Easing.CubicOut,
                        finished: (v, b) =>
                        {
                            d.Opacity = 0;
                            d.IsSelected = false;
                        });
                    }
                }
                SelectedDate = day.Date;
                this.AbortAnimation("selected");
                this.Animate("selected", v =>
                {
                    day.Opacity = v;
                },
                start: 0, end: 1,
                easing: Easing.CubicOut,
                finished: (v, b) =>
                {
                    day.Opacity = 1;
                });
                DateSelected?.Invoke(this, new DateEventArgs(day.Date));
            }
        }

        private void OnPanUpdated(object sender, PanUpdatedEventArgs e)
        {
            if (e.StatusType == GestureStatus.Started)
            {
                lastDate = DateTime.Now;
                x1 = null;
            }
            else if (e.StatusType == GestureStatus.Running)
            {
                if (x1 == null)
                {
                    x1 = e.TotalX;
                }
                x2 = e.TotalX;
            }
            else if (e.StatusType == GestureStatus.Completed)
            {
                if (x1 == null)
                {
                    return;
                }
                var totalX = x2 - x1.Value;
                x1 = null;
                var ms = (DateTime.Now - lastDate).TotalMilliseconds;
                var speed = totalX / ms;
                Helper.Debug($"completed, speed: {speed}");
                if (speed < -0.7)
                {
                    LocatedDate = LocatedDate.AddDays(7);
                }
                else if (speed > 0.7)
                {
                    LocatedDate = LocatedDate.AddDays(-7);
                }
                OnSelectedDatePropertyChanged(this, null, SelectedDate);
            }
        }
    }

    public class DateEventArgs : EventArgs
    {
        public DateTime Date { get; }

        public DateEventArgs(DateTime date)
        {
            Date = date;
        }
    }

    public class BillingDayView : ContentView
    {
        public static readonly BindableProperty BillingDayProperty = BindableProperty.Create(nameof(BillingDay), typeof(BillingDay), typeof(BillingDayView));
        public static readonly BindableProperty CommandProperty = BindableProperty.Create(nameof(Command), typeof(Command), typeof(BillingDayView));

        public BillingDay BillingDay
        {
            get => (BillingDay)GetValue(BillingDayProperty);
            set => SetValue(BillingDayProperty, value);
        }
        public Command Command
        {
            get => (Command)GetValue(CommandProperty);
            set => SetValue(CommandProperty, value);
        }
    }

    public class BillingDay : BindableObject
    {
        private static readonly BindableProperty DateProperty = BindableProperty.Create(nameof(Date), typeof(DateTime), typeof(BillingDay), propertyChanged: OnDatePropertyChanged);
        private static readonly BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(BillingDay));
        private static readonly BindableProperty FontFamilyProperty = BindableProperty.Create(nameof(FontFamily), typeof(string), typeof(BillingDay), defaultValue: Definition.GetCascadiaRegularFontFamily());
        private static readonly BindableProperty IsSelectedProperty = BindableProperty.Create(nameof(IsSelected), typeof(bool), typeof(BillingDay));
        private static readonly BindableProperty OpacityProperty = BindableProperty.Create(nameof(Opacity), typeof(double), typeof(BillingDay), defaultValue: 1.0);
        private static readonly BindableProperty TextOpacityProperty = BindableProperty.Create(nameof(TextOpacity), typeof(double), typeof(BillingDay), defaultValue: 1.0);

        private static void OnDatePropertyChanged(BindableObject obj, object old, object @new)
        {
            if (obj is BillingDay day && @new is DateTime date)
            {
                if (date.Day == 1)
                {
                    day.SetValue(TextProperty, date.ToString("MMM"));
                }
                else
                {
                    day.SetValue(TextProperty, date.Day.ToString());
                }
            }
        }

        public DateTime Date
        {
            get => (DateTime)GetValue(DateProperty);
            set => SetValue(DateProperty, value);
        }
        public string Text => (string)GetValue(TextProperty);
        public string FontFamily => (string)GetValue(FontFamilyProperty);
        public bool IsSelected
        {
            get => (bool)GetValue(IsSelectedProperty);
            set => SetValue(IsSelectedProperty, value);
        }
        public double Opacity
        {
            get => (double)GetValue(OpacityProperty);
            set => SetValue(OpacityProperty, value);
        }
        public double TextOpacity => (double)GetValue(TextOpacityProperty);

        public BillingDay(DateTime date)
        {
            Date = date;
        }

        public void Refresh(DateTime selected)
        {
            var date = Date;
            if (date.Year == selected.Year && date.DayOfYear == selected.DayOfYear)
            {
                SetValue(IsSelectedProperty, true);
                SetValue(FontFamilyProperty, Definition.GetCascadiaBoldFontFamily());
            }
            else
            {
                SetValue(FontFamilyProperty, Definition.GetCascadiaRegularFontFamily());
            }
            if (date.Year == selected.Year && date.Month == selected.Month)
            {
                SetValue(TextOpacityProperty, 1.0);
            }
            else
            {
                SetValue(TextOpacityProperty, .4);
            }
        }
    }
}