/// Credit Mrs. YakaYocha 
/// Sourced from - https://www.youtube.com/channel/UCHp8LZ_0-iCvl-5pjHATsgw
/// Please donate: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=RJ8D9FRFQF9VS

using UnityEngine.Events;

namespace UnityEngine.UI.Extensions
{
    [RequireComponent(typeof(ScrollRect))]
    [AddComponentMenu("Layout/Extensions/Vertical Scroller")]
    public class UIVerticalScroller : MonoBehaviour
    {
        [Tooltip("desired ScrollRect")]
        public ScrollRect scrollRect;
        [Tooltip("Center display area (position of zoomed content)")]
        public RectTransform center;
        [Tooltip("Size / spacing of elements")]
        public RectTransform elementSize;
        [Tooltip("Scale = 1/ (1+distance fom center * shrinkage)")]
        public Vector2 elementShrinkage = new Vector2(1f / 200, 1f / 200);
        [Tooltip("Minimum element scale (furthest from center)")]
        public Vector2 minScale = new Vector2(0.7f, 0.7f);
        [Tooltip("Select the item to be in center on start.")]
        public int startingIndex = -1;
        [Tooltip("Stop scrolling past last element from inertia.")]
        public bool stopMomentumOnEnd = true;
        [Tooltip("Set Items out of center to not interactible.")]
        public bool disableUnfocused = true;
        [Tooltip("Button to go to the next page. (optional)")]
        public GameObject scrollUpButton;
        [Tooltip("Button to go to the previous page. (optional)")]
        public GameObject scrollDownButton;
        [Tooltip("Event fired when a specific item is clicked, exposes index number of item. (optional)")]
        public IntEvent OnButtonClicked;
        [Tooltip("Event fired when the focused item is Changed. (optional)")]
        public IntEvent OnFocusChanged;
        [HideInInspector]
        public GameObject[] _arrayOfElements;

        public int focusedElementIndex { get; private set; }

        public string result { get; private set; }

        private float[] distReposition;
        private float[] distance;
        //private int elementsDistance;


        //Scrollable area (content of desired ScrollRect)
        [HideInInspector]
        public RectTransform scrollingPanel{ get { return scrollRect.content; } }


        /// <summary>
        /// Constructor when not used as component but called from other script, don't forget to set the non-optional properties.
        /// </summary>
        public UIVerticalScroller()
        {
        }

        /// <summary>
        /// Constructor when not used as component but called from other script
        /// </summary>
        public UIVerticalScroller(RectTransform center, RectTransform elementSize, ScrollRect scrollRect, GameObject[] arrayOfElements)
        {
            this.center = center;
            this.elementSize = elementSize;
            this.scrollRect = scrollRect;
            _arrayOfElements = arrayOfElements;
        }

        /// <summary>
        /// Awake this instance.
        /// </summary>
        public void Awake()
        {
            if (!scrollRect)
            {
                scrollRect = GetComponent<ScrollRect>();
            }
            if (!center)
            {
                Debug.LogError("Please define the RectTransform for the Center viewport of the scrollable area");
            }
            if (!elementSize)
            {
                elementSize = center;
            }
            if (_arrayOfElements == null || _arrayOfElements.Length == 0)
            {
                _arrayOfElements = new GameObject[scrollingPanel.childCount];
                for (int i = 0; i < scrollingPanel.childCount; i++)
                {
                    _arrayOfElements[i] = scrollingPanel.GetChild(i).gameObject;
                }     
            }
        }

        /// <summary>
        /// Recognises and resizes the children.
        /// </summary>
        /// <param name="startingIndex">Starting index.</param>
        /// <param name="arrayOfElements">Array of elements.</param>
        public void updateChildren(int startingIndex = -1, GameObject[] arrayOfElements = null)
        {
            // Set _arrayOfElements to arrayOfElements if given, otherwise to child objects of the scrolling panel.
            if (arrayOfElements != null)
            {
                _arrayOfElements = arrayOfElements;
            }
            else
            {
                _arrayOfElements = new GameObject[scrollingPanel.childCount];
                for (int i = 0; i < scrollingPanel.childCount; i++)
                {
                    _arrayOfElements[i] = scrollingPanel.GetChild(i).gameObject;
                }
            }

            // resize the elements to match elementSize rect
            for (var i = 0; i < _arrayOfElements.Length; i++)
            {
                int j = i;
                _arrayOfElements[i].GetComponent<Button>().onClick.RemoveAllListeners();
                if (OnButtonClicked != null)
                {
                    _arrayOfElements[i].GetComponent<Button>().onClick.AddListener(() => OnButtonClicked.Invoke(j));
                }
                RectTransform r = _arrayOfElements[i].GetComponent<RectTransform>();
                r.anchorMax = r.anchorMin = r.pivot = new Vector2(0.5f, 0.5f);
                r.localPosition = new Vector2(0, i * elementSize.rect.size.y);
                r.sizeDelta = elementSize.rect.size;
            }

            // prepare for scrolling
            distance = new float[_arrayOfElements.Length];
            distReposition = new float[_arrayOfElements.Length];
            focusedElementIndex = -1;

            //scrollRect.scrollSensitivity = elementSize.rect.height / 5;

            // if starting index is given, snap to respective element
            if (startingIndex > -1)
            {
                startingIndex = startingIndex > _arrayOfElements.Length ? _arrayOfElements.Length - 1 : startingIndex;
                SnapToElement(startingIndex);
            }
        }

        public void Start()
        {

            if (scrollUpButton)
                scrollUpButton.GetComponent<Button>().onClick.AddListener(() =>
                    {
                        ScrollUp();
                    });

            if (scrollDownButton)
                scrollDownButton.GetComponent<Button>().onClick.AddListener(() =>
                    {
                        ScrollDown();
                    });
            updateChildren(startingIndex, _arrayOfElements);
        }


        public void Update()
        {
            if (_arrayOfElements.Length < 1)
            {
                return;
            }

            for (var i = 0; i < _arrayOfElements.Length; i++)
            {
                distReposition[i] = center.GetComponent<RectTransform>().position.y - _arrayOfElements[i].GetComponent<RectTransform>().position.y;
                distance[i] = Mathf.Abs(distReposition[i]);

                //Magnifying effect
                Vector2 scale = Vector2.Max(minScale, new Vector2(1 / (1 + distance[i] * elementShrinkage.x), (1 / (1 + distance[i] * elementShrinkage.y))));
                _arrayOfElements[i].GetComponent<RectTransform>().transform.localScale = new Vector3(scale.x, scale.y, 1f);
            }

            // detect focused element
            float minDistance = Mathf.Min(distance);
            int oldFocusedElement = focusedElementIndex;
            for (var i = 0; i < _arrayOfElements.Length; i++)
            {
                _arrayOfElements[i].GetComponent<CanvasGroup>().interactable = !disableUnfocused || minDistance == distance[i];
                if (minDistance == distance[i])
                {
                    focusedElementIndex = i;
                    result = _arrayOfElements[i].GetComponentInChildren<Text>().text;
                }
            }
            if (focusedElementIndex != oldFocusedElement && OnFocusChanged != null)
            {
                OnFocusChanged.Invoke(focusedElementIndex);
            }


            if (!Input.GetMouseButton(0))
            {
                // scroll slowly to nearest element when not dragged
                ScrollingElements();
            }


            // stop scrolling past last element from inertia
            if (stopMomentumOnEnd
                && (_arrayOfElements[0].GetComponent<RectTransform>().position.y > center.position.y
                || _arrayOfElements[_arrayOfElements.Length - 1].GetComponent<RectTransform>().position.y < center.position.y))
            {
                scrollRect.velocity = Vector2.zero;
            }
        }

        private void ScrollingElements()
        {
            float newY = Mathf.Lerp(scrollingPanel.anchoredPosition.y, scrollingPanel.anchoredPosition.y + distReposition[focusedElementIndex], Time.deltaTime * 2f);
            Vector2 newPosition = new Vector2(scrollingPanel.anchoredPosition.x, newY);
            scrollingPanel.anchoredPosition = newPosition;
        }

        public void SnapToElement(int element)
        {
            float deltaElementPositionY = elementSize.rect.height * element;
            Vector2 newPosition = new Vector2(scrollingPanel.anchoredPosition.x, -deltaElementPositionY);
            scrollingPanel.anchoredPosition = newPosition;

        }

        public void ScrollUp()
        {
            float deltaUp = elementSize.rect.height / 1.2f;
            Vector2 newPositionUp = new Vector2(scrollingPanel.anchoredPosition.x, scrollingPanel.anchoredPosition.y - deltaUp);
            scrollingPanel.anchoredPosition = Vector2.Lerp(scrollingPanel.anchoredPosition, newPositionUp, 1);
        }

        public void ScrollDown()
        {
            float deltaDown = elementSize.rect.height / 1.2f;
            Vector2 newPositionDown = new Vector2(scrollingPanel.anchoredPosition.x, scrollingPanel.anchoredPosition.y + deltaDown);
            scrollingPanel.anchoredPosition = newPositionDown;
        }

        [System.Serializable]
        public class IntEvent:UnityEvent<int>
        {

        }
    }
}