using System;
using Xamarin.Forms;

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

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

        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 = Helper.Create<DateTime, BillingDate>(nameof(LocatedDate), propertyChanged: OnLocatedDatePropertyChanged);
        public static readonly BindableProperty SelectedDateProperty = Helper.Create<DateTime, BillingDate>(nameof(SelectedDate), propertyChanged: OnSelectedDatePropertyChanged);

        private static void OnLocatedDatePropertyChanged(BillingDate billingDate, DateTime old, DateTime date)
        {
            var week = (int)date.DayOfWeek;
            var tmpDate = date.AddDays(-week);
            for (var i = 0; i < 7; i++)
            {
                var day = new BillingDay(tmpDate);
                billingDate[i] = day;
                tmpDate = tmpDate.AddDays(1);
            }
        }

        private static void OnSelectedDatePropertyChanged(BillingDate billingDate, DateTime old, DateTime selected)
        {
            for (var i = 0; i < 7; i++)
            {
                var day = billingDate[i];
                day.Refresh(selected);
            }
        }

        public BillingDay this[int index]
        {
            get => (BillingDay)GetValue(GetWeekProperty(index));
            set => SetValue(GetWeekProperty(index), value);
        }

        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 (Helper.IsSameDay(selectedDate, SelectedDate))
            {
                return;
            }
            DateTime located;
            if (locatedDate != null)
            {
                located = Helper.FirstDay(locatedDate.Value);
            }
            else
            {
                located = Helper.FirstDay(selectedDate);
            }
            if (LocatedDate != located)
            {
                LocatedDate = located;
            }
            else
            {
                RestoreState(this[(int)selectedDate.DayOfWeek]);
            }
            SelectedDate = selectedDate;
            DateSelected?.Invoke(this, new DateEventArgs(selectedDate));
        }

        private void OnDayChanged(object o)
        {
            var selected = SelectedDate;
            if (o is BillingDay day && !Helper.IsSameDay(selected, day.Date))
            {
                RestoreState(day);
                SelectedDate = day.Date;
                DateSelected?.Invoke(this, new DateEventArgs(day.Date));
            }
        }

        private void RestoreState(BillingDay day)
        {
            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;
                    });
                }
            }
            this.AbortAnimation("selected");
            this.Animate("selected", v =>
            {
                day.Opacity = v;
            },
            start: 0, end: 1,
            easing: Easing.CubicOut,
            finished: (v, b) =>
            {
                day.Opacity = 1;
            });
        }

        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;
                if (speed < -0.7)
                {
                    LocatedDate = LocatedDate.AddDays(7);
                }
                else if (speed > 0.7)
                {
                    LocatedDate = LocatedDate.AddDays(-7);
                }
                OnSelectedDatePropertyChanged(this, default, SelectedDate);
            }
        }
    }

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

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

    public class BillingDayView : ContentView
    {
        public static readonly BindableProperty BillingDayProperty = Helper.Create<BillingDay, BillingDayView>(nameof(BillingDay));

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

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

        private static void OnDatePropertyChanged(BillingDay day, DateTime old, DateTime date)
        {
            if (date.Day == 1)
            {
                day.SetValue(TextProperty, date.ToString("MMM"));
            }
            else
            {
                day.SetValue(TextProperty, date.Day.ToString());
            }
        }

        public DateTime Date => (DateTime)GetValue(DateProperty);
        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)
        {
            SetValue(DateProperty, date);
        }

        public void Refresh(DateTime selected)
        {
            var date = Date;
            if (Helper.IsSameDay(date, selected))
            {
                IsSelected = true;
                SetValue(FontFamilyProperty, Definition.GetBoldFontFamily());
            }
            else
            {
                SetValue(FontFamilyProperty, Definition.GetRegularFontFamily());
            }
            if (date.Year == selected.Year && date.Month == selected.Month)
            {
                SetValue(TextOpacityProperty, 1.0);
            }
            else
            {
                SetValue(TextOpacityProperty, .4);
            }
        }
    }
}