using System;

using System.Collections;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using PanCardView.Behaviors;
using PanCardView.Enums;
using PanCardView.Extensions;
using PanCardView.Utility;
using Xamarin.Forms;
using static System.Math;

namespace PanCardView.Controls
{
    public class TabsControl : AbsoluteLayout
    {
        public static readonly BindableProperty DiffProperty = BindableProperty.Create(nameof(Diff), typeof(double), typeof(TabsControl), 0.0, propertyChanged: (bindable, oldValue, newValue) =>
        {
            bindable.AsTabsView().UpdateStripePosition();
        });

        public static readonly BindableProperty MaxDiffProperty = BindableProperty.Create(nameof(MaxDiff), typeof(double), typeof(TabsControl), 0.0, propertyChanged: (bindable, oldValue, newValue) =>
        {
            bindable.AsTabsView().UpdateStripePosition();
        });

        public static readonly BindableProperty ItemsCountProperty = BindableProperty.Create(nameof(ItemsCount), typeof(int), typeof(TabsControl), -1);

        public static readonly BindableProperty SelectedIndexProperty = BindableProperty.Create(nameof(SelectedIndex), typeof(int), typeof(TabsControl), 0, propertyChanged: (bindable, oldValue, newValue) =>
        {
            bindable.AsTabsView().OnSelectedIndexChanged();
        });

        public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create(nameof(ItemTemplate), typeof(DataTemplate), typeof(TabsControl), null, propertyChanged: (bindable, oldValue, newValue) =>
        {
            bindable.AsTabsView().ResetItemsLayout();
        });

        public static readonly BindableProperty StripeColorProperty = BindableProperty.Create(nameof(StripeColor), typeof(Color), typeof(TabsControl), Color.CadetBlue, propertyChanged: (bindable, oldValue, newValue) =>
        {
            bindable.AsTabsView().ResetStripeView();
        });

        public static readonly BindableProperty StripeHeightProperty = BindableProperty.Create(nameof(StripeHeight), typeof(double), typeof(TabsControl), 3.0, propertyChanged: (bindable, oldValue, newValue) =>
        {
            bindable.AsTabsView().ResetStripeView();
        });

        public static readonly BindableProperty IsCyclicalProperty = BindableProperty.Create(nameof(IsCyclical), typeof(bool), typeof(TabsControl), false, propertyChanged: (bindable, oldValue, newValue) =>
        {
            bindable.AsTabsView().ResetItemsLayout();
        });

        public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(nameof(ItemsSource), typeof(IEnumerable), typeof(TabsControl), null, propertyChanged: (bindable, oldValue, newValue) =>
        {
            bindable.AsTabsView().ResetItemsSource(oldValue as IEnumerable);
        });

        public static readonly BindableProperty IsUserInteractionRunningProperty = BindableProperty.Create(nameof(IsUserInteractionRunning), typeof(bool), typeof(TabsControl), true, propertyChanged: (bindable, oldValue, newValue) =>
        {
            bindable.AsTabsView().ResetVisibility();
        });

        public static readonly BindableProperty IsAutoInteractionRunningProperty = BindableProperty.Create(nameof(IsAutoInteractionRunning), typeof(bool), typeof(TabsControl), true, propertyChanged: (bindable, oldValue, newValue) =>
        {
            bindable.AsTabsView().ResetVisibility();
        });

        public static readonly BindableProperty ToFadeDurationProperty = BindableProperty.Create(nameof(ToFadeDuration), typeof(int), typeof(TabsControl), 0);

        public static readonly BindableProperty UseParentAsBindingContextProperty = BindableProperty.Create(nameof(UseParentAsBindingContext), typeof(bool), typeof(TabsControl), true);

        public static readonly BindableProperty StripePositionProperty = BindableProperty.Create(nameof(StripePosition), typeof(StripePosition), typeof(TabsControl), StripePosition.Bottom, propertyChanged: (bindable, oldValue, newValue) =>
        {
            bindable.AsTabsView().ResetItemsLayout();
        });

        static TabsControl()
        {
        }

        private CancellationTokenSource _fadeAnimationTokenSource;

        public TabsControl()
        {
            Children.Add(ItemsStackLayout, new Rectangle(0, 0, 1, 1), AbsoluteLayoutFlags.All);
            Children.Add(MainStripeView, new Rectangle(0, 1, 0, 0), AbsoluteLayoutFlags.YProportional);
            Children.Add(AdditionalStripeView, new Rectangle(0, 1, 0, 0), AbsoluteLayoutFlags.YProportional);

            this.SetBinding(DiffProperty, nameof(CardsView.ProcessorDiff));
            this.SetBinding(MaxDiffProperty, nameof(Width));
            this.SetBinding(SelectedIndexProperty, nameof(CardsView.SelectedIndex));
            this.SetBinding(ItemsSourceProperty, nameof(CardsView.ItemsSource));
            this.SetBinding(IsCyclicalProperty, nameof(CardsView.IsCyclical));
            this.SetBinding(IsUserInteractionRunningProperty, nameof(CardsView.IsUserInteractionRunning));
            this.SetBinding(IsAutoInteractionRunningProperty, nameof(CardsView.IsAutoInteractionRunning));

            SetLayoutBounds(this, new Rectangle(.5, 1, -1, -1));
            SetLayoutFlags(this, AbsoluteLayoutFlags.PositionProportional);
            Behaviors.Add(new ProtectedControlBehavior());
        }

        private StackLayout ItemsStackLayout { get; } = new StackLayout
        {
            Spacing = 0,
            Orientation = StackOrientation.Horizontal
        };

        private BoxView MainStripeView { get; set; } = new BoxView();

        private BoxView AdditionalStripeView { get; set; } = new BoxView();

        public double Diff
        {
            get => (double)GetValue(DiffProperty);
            set => SetValue(DiffProperty, value);
        }

        public double MaxDiff
        {
            get => (double)GetValue(MaxDiffProperty);
            set => SetValue(MaxDiffProperty, value);
        }

        public int ItemsCount
        {
            get => (int)GetValue(ItemsCountProperty);
            set => SetValue(ItemsCountProperty, value);
        }

        public int SelectedIndex
        {
            get => (int)GetValue(SelectedIndexProperty);
            set => SetValue(SelectedIndexProperty, value);
        }

        public DataTemplate ItemTemplate
        {
            get => GetValue(ItemTemplateProperty) as DataTemplate;
            set => SetValue(ItemTemplateProperty, value);
        }

        public Color StripeColor
        {
            get => (Color)GetValue(StripeColorProperty);
            set => SetValue(StripeColorProperty, value);
        }

        public double StripeHeight
        {
            get => (double)GetValue(StripeHeightProperty);
            set => SetValue(StripeHeightProperty, value);
        }

        public bool IsCyclical
        {
            get => (bool)GetValue(IsCyclicalProperty);
            set => SetValue(IsCyclicalProperty, value);
        }

        public IEnumerable ItemsSource
        {
            get => GetValue(ItemsSourceProperty) as IEnumerable;
            set => SetValue(ItemsSourceProperty, value);
        }

        public bool IsUserInteractionRunning
        {
            get => (bool)GetValue(IsUserInteractionRunningProperty);
            set => SetValue(IsUserInteractionRunningProperty, value);
        }

        public bool IsAutoInteractionRunning
        {
            get => (bool)GetValue(IsAutoInteractionRunningProperty);
            set => SetValue(IsAutoInteractionRunningProperty, value);
        }

        public int ToFadeDuration
        {
            get => (int)GetValue(ToFadeDurationProperty);
            set => SetValue(ToFadeDurationProperty, value);
        }

        public bool UseParentAsBindingContext
        {
            get => (bool)GetValue(UseParentAsBindingContextProperty);
            set => SetValue(UseParentAsBindingContextProperty, value);
        }

        public StripePosition StripePosition
        {
            get => (StripePosition)GetValue(StripePositionProperty);
            set => SetValue(StripePositionProperty, value);
        }

        public object this[int index] => ItemsSource?.FindValue(index);

        [EditorBrowsable(EditorBrowsableState.Never)]
        public static void Preserve()
        {
        }

        protected virtual async void ResetVisibility(uint? appearingTime = null, Easing appearingEasing = null, uint? dissappearingTime = null, Easing disappearingEasing = null)
        {
            _fadeAnimationTokenSource?.Cancel();

            if (ItemsCount == 0)
            {
                Opacity = 0;
                IsVisible = false;
                return;
            }

            if (ToFadeDuration <= 0)
            {
                Opacity = 1;
                IsVisible = true;
                return;
            }

            if (IsUserInteractionRunning || IsAutoInteractionRunning)
            {
                IsVisible = true;

                await new AnimationWrapper(v => Opacity = v, Opacity, 1)
                    .Commit(this, nameof(ResetVisibility), 16, appearingTime ?? 330, appearingEasing ?? Easing.CubicInOut);
                return;
            }

            _fadeAnimationTokenSource = new CancellationTokenSource();
            var token = _fadeAnimationTokenSource.Token;

            await Task.Delay(ToFadeDuration);
            if (token.IsCancellationRequested)
            {
                return;
            }

            await new AnimationWrapper(v => Opacity = v, Opacity, 0)
                .Commit(this, nameof(ResetVisibility), 16, dissappearingTime ?? 330, disappearingEasing ?? Easing.SinOut);

            if (token.IsCancellationRequested)
            {
                return;
            }
            IsVisible = false;
        }

        protected override void OnParentSet()
        {
            base.OnParentSet();
            if (UseParentAsBindingContext && Parent is CardsView)
            {
                BindingContext = Parent;
            }
        }

        protected override void OnBindingContextChanged()
        {
            base.OnBindingContextChanged();
            ResetItemsLayout();
        }

        protected override void OnSizeAllocated(double width, double height)
        {
            base.OnSizeAllocated(width, height);
            UpdateStripePosition();
        }

        private void ResetItemsLayout()
        {
            if (Parent == null)
            {
                return;
            }

            try
            {
                BatchBegin();
                ItemsStackLayout.Children.Clear();
                if (ItemsSource == null)
                {
                    return;
                }

                ItemsCount = ItemsSource.Count();
                foreach (var item in ItemsSource)
                {
                    var view = ItemTemplate?.SelectTemplate(item)?.CreateView() ?? item as View;
                    if (view == null)
                    {
                        return;
                    }

                    if (!Equals(view, item))
                    {
                        view.BindingContext = item;
                    }

                    view.GestureRecognizers.Clear();
                    view.GestureRecognizers.Add(new TapGestureRecognizer
                    {
                        CommandParameter = item,
                        Command = new Command(p =>
                        {
                            this.SetBinding(SelectedIndexProperty, nameof(CardsView.SelectedIndex), BindingMode.OneWayToSource);
                            SelectedIndex = ItemsSource.FindIndex(p);
                            this.SetBinding(SelectedIndexProperty, nameof(CardsView.SelectedIndex));
                        })
                    });
                    ItemsStackLayout.Children.Add(view);
                }

                ResetStripeViewNonBatch();
                UpdateStripePositionNonBatch();
            }
            finally
            {
                BatchCommit();
            }
        }

        private void ResetItemsSource(IEnumerable oldCollection)
        {
            if (oldCollection is INotifyCollectionChanged oldObservableCollection)
            {
                oldObservableCollection.CollectionChanged -= OnObservableCollectionChanged;
            }

            if (ItemsSource is INotifyCollectionChanged observableCollection)
            {
                observableCollection.CollectionChanged += OnObservableCollectionChanged;
            }

            OnObservableCollectionChanged(null, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        }

        private void OnObservableCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        => ResetItemsLayout();

        private void ResetStripeView()
        {
            try
            {
                BatchBegin();
                ResetStripeViewNonBatch();
            }
            finally
            {
                BatchCommit();
            }
        }

        private void ResetStripeViewNonBatch()
        {
            ItemsStackLayout.Margin = new Thickness(0, 0, 0, StripeHeight);
            MainStripeView.Color = StripeColor;
            AdditionalStripeView.Color = StripeColor;
        }

        private void UpdateStripePosition()
        {
            try
            {
                BatchBegin();
                UpdateStripePositionNonBatch();
            }
            finally
            {
                BatchCommit();
            }
        }

        private void UpdateStripePositionNonBatch()
        {
            var itemsCount = ItemsCount;
            var selectedIndex = SelectedIndex.ToCyclicalIndex(itemsCount);
            var maxDiff = MaxDiff;

            if (selectedIndex < 0 || maxDiff < 0)
            {
                return;
            }

            var diff = Diff;
            var affectedIndex = diff < 0
                    ? (selectedIndex + 1)
                    : diff > 0
                        ? (selectedIndex - 1)
                        : selectedIndex;

            if (IsCyclical)
            {
                affectedIndex = affectedIndex.ToCyclicalIndex(itemsCount);
            }

            if (affectedIndex < 0 || affectedIndex >= ItemsStackLayout.Children.Count)
            {
                return;
            }

            var itemProgress = Min(Abs(diff) / maxDiff, 1);

            var currentItemView = ItemsStackLayout.Children[selectedIndex];
            var affectedItemView = ItemsStackLayout.Children[affectedIndex];
            if (diff <= 0)
            {
                CalculateStripePosition(currentItemView, affectedItemView, itemProgress, selectedIndex > affectedIndex);
                return;
            }
            CalculateStripePosition(affectedItemView, currentItemView, 1 - itemProgress, selectedIndex < affectedIndex);
        }

        private void CalculateStripePosition(View firstView, View secondView, double itemProgress, bool isSecondStripeVisible)
        {
            if(itemProgress <= 0 &&
                GetLayoutBounds(AdditionalStripeView).Width >
                GetLayoutBounds(MainStripeView).Width)
            {
                SwapStripeViews();
            }

            AdditionalStripeView.IsVisible = isSecondStripeVisible;
            var additionalStripeWidth = isSecondStripeVisible ? secondView.Width * itemProgress : 0;
            SetLayoutBounds(AdditionalStripeView, new Rectangle(secondView.X, StripePosition == StripePosition.Bottom ? 1 : 0, additionalStripeWidth, StripeHeight));

            var x = firstView.X + firstView.Width * itemProgress;
            var mainStripewidth = firstView.Width * (1 - itemProgress) + secondView.Width * itemProgress - additionalStripeWidth;
            SetLayoutBounds(MainStripeView, new Rectangle(x, StripePosition == StripePosition.Bottom ? 1 : 0, mainStripewidth, StripeHeight));
        }

        private void SwapStripeViews()
        {
            var view = MainStripeView;
            MainStripeView = AdditionalStripeView;
            AdditionalStripeView = view;
        }

        private void OnSelectedIndexChanged()
        {
            if(!(Parent is ScrollView scroll))
            {
                return;
            }
            var index = SelectedIndex.ToCyclicalIndex(ItemsCount);
            if(index < 0)
            {
                return;
            }
            scroll.ScrollToAsync(ItemsStackLayout.Children[index], ScrollToPosition.MakeVisible, true);
        }
    }
}