/// Credit BinaryX 
/// Sourced from - http://forum.unity3d.com/threads/scripts-useful-4-6-scripts-collection.264161/page-2#post-1945602
/// Updated by ddreaper - removed dependency on a custom ScrollRect script. Now implements drag interfaces and standard Scroll Rect.
/// Update by xesenix - rewrited almost entire code 
/// - configuration for direction move instead of 2 concurrent class (easiear to change direction in editor)
/// - supports list layouted with horizontal or vertical layout need to match direction with type of layout used
/// - dynamicly checks if scrolled list size changes and recalculates anchor positions 
///   and item size based on itemsVisibleAtOnce and size of root container
///   if you dont wish to use this auto resize turn of autoLayoutItems
/// - fixed current page made it independant from pivot
/// - replaced pagination with delegate function

using System;
using UnityEngine.EventSystems;

namespace UnityEngine.UI.Extensions
{
	[ExecuteInEditMode]
    [RequireComponent(typeof(ScrollRect))]
    [AddComponentMenu("UI/Extensions/Scroll Snap")]
    public class ScrollSnap : MonoBehaviour, IBeginDragHandler, IEndDragHandler, IDragHandler
	{
		// needed becouse of reversed behavior of axis Y compared to X
		// (positions of children lower in children list in horizontal directions grows when in vertical it gets smaller)
		public enum ScrollDirection {
			Horizontal,
			Vertical
		};

		public delegate void PageSnapChange(int page);

		public event PageSnapChange onPageChange;

		public ScrollDirection direction = ScrollDirection.Horizontal;

		protected ScrollRect scrollRect;
		protected RectTransform scrollRectTransform;
		protected Transform listContainerTransform;
		protected RectTransform rectTransform;

		protected int items = 0;

		int pages;

		protected int startingPage = 0;

		// anchor points to lerp to to see child on certain indexes
		protected Vector3[] pageAnchorPositions;
		protected Vector3 lerpTarget;
		protected bool lerp;

		// item list related
		protected float listContainerMinPosition;
		protected float listContainerMaxPosition;
		protected float listContainerSize;
		protected RectTransform listContainerRectTransform;
		protected Vector2 listContainerCachedSize;

		protected float itemSize;

        [Tooltip("Button to go to the next page. (optional)")]
        public GameObject nextButton;
        [Tooltip("Button to go to the previous page. (optional)")]
		public GameObject prevButton;
		
		[Tooltip("Number of items visible in one page of scroll frame.")]
		[RangeAttribute(1,100)]
		public int itemsVisibleAtOnce = 1;
		
		[Tooltip("Sets minimum width of list items to 1/itemsVisibleAtOnce.")]
		public bool autoLayoutItems = true;
		
		[Tooltip("If you wish to update scrollbar numberOfSteps to number of active children on list.")]
		public bool linkScrolbarSteps = false;

        public Boolean useFastSwipe = true;
        public int fastSwipeThreshold = 100;

		// drag related
		protected bool startDrag = true;
		protected Vector3 positionOnDragStart = new Vector3();
		protected int pageOnDragStart;
		
		protected bool fastSwipeTimer = false;
		protected int fastSwipeCounter = 0;
		protected int fastSwipeTarget = 10;

        // Use this for initialization
		void Start()
		{
			lerp = false;

			scrollRect = gameObject.GetComponent<ScrollRect> ();
			scrollRectTransform = gameObject.GetComponent<RectTransform> ();
			listContainerTransform = scrollRect.content;
			listContainerRectTransform = listContainerTransform.GetComponent<RectTransform> ();

			rectTransform = listContainerTransform.gameObject.GetComponent<RectTransform> ();
			UpdateListItemsSize();
			UpdateListItemPositions();

			ChangePage (CurrentPage ());

			if (nextButton)
			{
				nextButton.GetComponent<Button> ().onClick.AddListener (() => { NextScreen (); });
			}

            if (prevButton)
			{
				prevButton.GetComponent<Button> ().onClick.AddListener (() => { PreviousScreen (); });
			}
		}

		public void UpdateListItemsSize()
		{
			float size = 0;
			if (direction == ScrollSnap.ScrollDirection.Horizontal)
			{
				size = scrollRectTransform.rect.width / itemsVisibleAtOnce;
			}
			else
			{
				size = scrollRectTransform.rect.height / itemsVisibleAtOnce;
			}

			itemSize = size;

			if (autoLayoutItems && size != itemSize)
			{
				if (direction == ScrollSnap.ScrollDirection.Horizontal)
				{
					foreach (var tr in listContainerTransform)
					{
						GameObject child = ((Transform)tr).gameObject;
						if (child.activeInHierarchy)
						{
							var childLayout = child.GetComponent<LayoutElement> ();
							
							if (childLayout == null)
							{
								childLayout = child.AddComponent<LayoutElement> ();
							}
							
							childLayout.minWidth = itemSize;
						}
					}
				}
				else
				{
					foreach (var tr in listContainerTransform)
					{
						GameObject child = ((Transform)tr).gameObject;
						if (child.activeInHierarchy)
						{
							var childLayout = child.GetComponent<LayoutElement> ();
							
							if (childLayout == null)
							{
								childLayout = child.AddComponent<LayoutElement> ();
							}
							
							childLayout.minHeight = itemSize;
						}
					}
				}
			}
		}
		
		public void UpdateListItemPositions()
		{
			if (!listContainerRectTransform.rect.size.Equals(listContainerCachedSize))
			{
				// checking how many children of list are active
				int activeCount = 0;
				
				foreach (var tr in listContainerTransform) {
					if (((Transform)tr).gameObject.activeInHierarchy) {
						activeCount++;
					}
				}

				// if anything changed since last check reinitialize anchors list
				items = 0;
				Array.Resize(ref pageAnchorPositions, activeCount);
				
				if (activeCount > 0)
				{
					pages = Mathf.Max (activeCount - itemsVisibleAtOnce + 1, 1);

					if (direction == ScrollDirection.Horizontal)
					{
						// looking for list spanning range min/max
						scrollRect.horizontalNormalizedPosition = 0;
						listContainerMaxPosition = listContainerTransform.localPosition.x;
						scrollRect.horizontalNormalizedPosition = 1;
						listContainerMinPosition = listContainerTransform.localPosition.x;
						
						listContainerSize = listContainerMaxPosition - listContainerMinPosition;

						for (var i = 0; i < pages; i ++)
						{
							pageAnchorPositions[i] = new Vector3(
								listContainerMaxPosition - itemSize * i,
								listContainerTransform.localPosition.y,
								listContainerTransform.localPosition.z
							);
						}
					}
					else
					{
						// looking for list spanning range
						scrollRect.verticalNormalizedPosition = 1;
						listContainerMinPosition = listContainerTransform.localPosition.y;
						scrollRect.verticalNormalizedPosition = 0;
						listContainerMaxPosition = listContainerTransform.localPosition.y;
						
						listContainerSize = listContainerMaxPosition - listContainerMinPosition;

						for (var i = 0; i < pages; i ++)
						{
							pageAnchorPositions[i] = new Vector3(
								listContainerTransform.localPosition.x,
								listContainerMinPosition + itemSize * i,
								listContainerTransform.localPosition.z
							);
						}
						
						foreach (var tr in listContainerTransform) {
							if (((Transform)tr).gameObject.activeInHierarchy) {
								activeCount++;
							}
						}

					}

					UpdateScrollbar(linkScrolbarSteps);
					startingPage = Mathf.Min(startingPage, pages);
					ResetPage();
				}
				items = activeCount;
				listContainerCachedSize.Set (listContainerRectTransform.rect.size.x, listContainerRectTransform.rect.size.y); 
			}

		}

		public void ResetPage()
		{
			if (direction == ScrollDirection.Horizontal)
			{
				scrollRect.horizontalNormalizedPosition = pages > 1 ? (float)startingPage / (float)(pages - 1) : 0;
			}
			else
			{
				scrollRect.verticalNormalizedPosition = pages > 1 ? (float)(pages - startingPage - 1) / (float)(pages - 1) : 0;
			}
		}

		protected void UpdateScrollbar(bool linkSteps)
		{
			if (linkSteps)
			{
				if (direction == ScrollDirection.Horizontal)
				{
					if (scrollRect.horizontalScrollbar != null)
					{
						scrollRect.horizontalScrollbar.numberOfSteps = pages;
					}
				}
				else
				{
					if (scrollRect.verticalScrollbar != null)
					{
						scrollRect.verticalScrollbar.numberOfSteps = pages;
					}
				}
			}
			else
			{
				if (direction == ScrollDirection.Horizontal)
				{
					if (scrollRect.horizontalScrollbar != null)
					{
						scrollRect.horizontalScrollbar.numberOfSteps = 0;
					}
				}
				else
				{
					if (scrollRect.verticalScrollbar != null)
					{
						scrollRect.verticalScrollbar.numberOfSteps = 0;
					}
				}
			}
		}

        void Update()
		{
			UpdateListItemsSize();
			UpdateListItemPositions();

			if (lerp)
			{
				UpdateScrollbar(false);

                listContainerTransform.localPosition = Vector3.Lerp(listContainerTransform.localPosition, lerpTarget, 7.5f * Time.deltaTime);
                
				if (Vector3.Distance(listContainerTransform.localPosition, lerpTarget) < 0.001f)
				{
					listContainerTransform.localPosition = lerpTarget;
					lerp = false;
					
					UpdateScrollbar(linkScrolbarSteps);
                }

                //change the info bullets at the bottom of the screen. Just for visual effect
                if (Vector3.Distance(listContainerTransform.localPosition, lerpTarget) < 10f)
				{
                    ChangePage(CurrentPage());
                }
            }

            if (fastSwipeTimer)
			{
                fastSwipeCounter++;
			}
        }

        private bool fastSwipe = false; //to determine if a fast swipe was performed


        //Function for switching screens with buttons
        public void NextScreen()
		{
			UpdateListItemPositions ();

			if (CurrentPage() < pages - 1)
			{
                lerp = true;
                lerpTarget = pageAnchorPositions[CurrentPage() + 1];

                ChangePage(CurrentPage() + 1);
            }
        }

        //Function for switching screens with buttons
        public void PreviousScreen()
		{
			UpdateListItemPositions ();

			if (CurrentPage() > 0)
			{
                lerp = true;
                lerpTarget = pageAnchorPositions[CurrentPage() - 1];

                ChangePage(CurrentPage() - 1);
            }
        }

        //Because the CurrentScreen function is not so reliable, these are the functions used for swipes
        private void NextScreenCommand()
        {
			if (pageOnDragStart < pages - 1)
			{
				int targetPage = Mathf.Min(pages - 1, pageOnDragStart + itemsVisibleAtOnce);
                lerp = true;

				lerpTarget = pageAnchorPositions[targetPage];

				ChangePage(targetPage);
            }
        }

        //Because the CurrentScreen function is not so reliable, these are the functions used for swipes
        private void PrevScreenCommand()
		{
			if (pageOnDragStart > 0)
			{
				int targetPage = Mathf.Max(0, pageOnDragStart - itemsVisibleAtOnce);
                lerp = true;

				lerpTarget = pageAnchorPositions[targetPage];

				ChangePage(targetPage);
            }
        }


        //returns the current screen that the is seeing
        public int CurrentPage()
		{
			float pos;

			if (direction == ScrollDirection.Horizontal)
			{
				pos = listContainerMaxPosition - listContainerTransform.localPosition.x;
				pos = Mathf.Clamp(pos, 0, listContainerSize);
			}
			else
			{
				pos = listContainerTransform.localPosition.y - listContainerMinPosition;
				pos = Mathf.Clamp(pos, 0, listContainerSize);
			}

			float page = pos / itemSize;

			return Mathf.Clamp(Mathf.RoundToInt(page), 0, pages);
        }

        //changes the bullets on the bottom of the page - pagination
        private void ChangePage(int currentPage)
        {
			startingPage = currentPage;

			if (onPageChange != null)
			{
				onPageChange(currentPage);
			}
        }

        #region Interfaces
        public void OnBeginDrag(PointerEventData eventData)
        {
			UpdateScrollbar(false);

			fastSwipeCounter = 0;
			fastSwipeTimer = true;

			positionOnDragStart = eventData.position;
            pageOnDragStart = CurrentPage();
        }

        public void OnEndDrag(PointerEventData eventData)
        {
            startDrag = true;
			float change = 0;

			if (direction == ScrollDirection.Horizontal)
			{
				change = positionOnDragStart.x - eventData.position.x;
			}
			else
			{
				change = -positionOnDragStart.y + eventData.position.y;
			}

            if (useFastSwipe)
            {
                fastSwipe = false;
                fastSwipeTimer = false;

                if (fastSwipeCounter <= fastSwipeTarget)
                {
                    if (Math.Abs(change) > fastSwipeThreshold)
                    {
                        fastSwipe = true;
                    }
                }
                if (fastSwipe)
                {
					if (change > 0)
					{
						NextScreenCommand();
					}
					else
					{
						PrevScreenCommand();
					}
                }
                else
                {
                    lerp = true;
					lerpTarget = pageAnchorPositions[CurrentPage()];
                }
            }
            else
            {
                lerp = true;
				lerpTarget = pageAnchorPositions[CurrentPage()];
            }
        }

        public void OnDrag(PointerEventData eventData)
        {
			lerp = false;

			if (startDrag)
            {
                OnBeginDrag(eventData);
                startDrag = false;
            }
        }
        #endregion
    }
}