/// Credit Ben MacKinnon @Dover8
/// Sourced from - https://github.com/Dover8/Unity-UI-Extensions/tree/range-slider
/// Usage: Extension of the standard slider. Two handles determine a low and high value between a Min and Max. 
/// Raises a UnityEvent passing the low and high values

using System;
using UnityEngine.Events;
using UnityEngine.EventSystems;

namespace UnityEngine.UI.Extensions
{
    [ExecuteInEditMode]
    [RequireComponent(typeof(RectTransform))]
    [AddComponentMenu("UI/Extensions/Sliders/Range Slider", 34)]
    public class RangeSlider : Selectable, IDragHandler, IInitializePotentialDragHandler, ICanvasElement
    {
        public enum Direction
        {
            Horizontal,
            Vertical
        }

        [Serializable]
        public class RangeSliderEvent : UnityEvent<float, float>
        {
        }

        [SerializeField] private RectTransform m_FillRect;

        public RectTransform FillRect
        {
            get { return m_FillRect; }
            set
            {
                if (SetClass(ref m_FillRect, value))
                {
                    UpdateCachedReferences();
                    UpdateVisuals();
                }
            }
        }

        [SerializeField] private RectTransform m_LowHandleRect;

        public RectTransform LowHandleRect
        {
            get { return m_LowHandleRect; }
            set
            {
                if (SetClass(ref m_LowHandleRect, value))
                {
                    UpdateCachedReferences();
                    UpdateVisuals();
                }
            }
        }

        [SerializeField] private RectTransform m_HighHandleRect;

        public RectTransform HighHandleRect
        {
            get { return m_HighHandleRect; }
            set
            {
                if (SetClass(ref m_HighHandleRect, value))
                {
                    UpdateCachedReferences();
                    UpdateVisuals();
                }
            }
        }

        [Space] [SerializeField] private Direction m_Direction = Direction.Horizontal;

        public Direction direction
        {
            get { return m_Direction; }
            set
            {
                if (SetPropertyUtility.SetStruct(ref m_Direction, value)) UpdateVisuals();
            }
        }

        [SerializeField] private float m_MinValue = 0;

        public float MinValue
        {
            get { return m_MinValue; }
            set
            {
                if (SetStruct(ref m_MinValue, value))
                {
                    SetLow(m_LowValue);
                    SetHigh(m_HighValue);
                    UpdateVisuals();
                }
            }
        }


        [SerializeField] private float m_MaxValue = 1;

        public float MaxValue
        {
            get { return m_MaxValue; }
            set
            {
                if (SetStruct(ref m_MaxValue, value))
                {
                    SetLow(m_LowValue);
                    SetHigh(m_HighValue);
                    UpdateVisuals();
                }
            }
        }

        [SerializeField] private bool m_WholeNumbers = false;

        public bool WholeNumbers
        {
            get { return m_WholeNumbers; }
            set
            {
                if (SetStruct(ref m_WholeNumbers, value))
                {
                    SetLow(m_LowValue);
                    SetHigh(m_HighValue);
                    UpdateVisuals();
                }
            }
        }

        [SerializeField] private float m_LowValue;

        public virtual float LowValue
        {
            get
            {
                if (WholeNumbers)
                {
                    return Mathf.Round(m_LowValue);
                }

                return m_LowValue;
            }
            set
            {
                SetLow(value);
            }
        }

        public float NormalizedLowValue
        {
            get
            {
                if (Mathf.Approximately(MinValue, MaxValue))
                {
                    return 0;
                }

                return Mathf.InverseLerp(MinValue, MaxValue, LowValue);
            }
            set
            {
                this.LowValue = Mathf.Lerp(MinValue, MaxValue, value);
            }
        }


        [SerializeField] private float m_HighValue;

        public virtual float HighValue
        {
            get
            {
                if (WholeNumbers)
                {
                    return Mathf.Round(m_HighValue);
                }

                return m_HighValue;
            }
            set
            {
                SetHigh(value);
            }
        }

        public float NormalizedHighValue
        {
            get
            {
                if (Mathf.Approximately(MinValue, MaxValue))
                {
                    return 0;
                }

                return Mathf.InverseLerp(MinValue, MaxValue, HighValue);
            }
            set
            {
                this.HighValue = Mathf.Lerp(MinValue, MaxValue, value);
            }
        }

        /// <summary>
        /// Set the value of the slider without invoking onValueChanged callback.
        /// </summary>
        /// <param name="input">The new value for the slider.</param>
        public virtual void SetValueWithoutNotify(float low, float high)
        {
            SetLow(low, false);
            SetHigh(high, false);
        }

        [Space] [SerializeField] private RangeSliderEvent m_OnValueChanged = new RangeSliderEvent();

        public RangeSliderEvent OnValueChanged { get { return m_OnValueChanged; } set { m_OnValueChanged = value; } }

        // Private fields

        /// <summary>
        /// An Enum that says in what state we and interacting with the slider
        /// </summary>
        private enum InteractionState
        {
            Low,
            High,
            Bar,
            None
        }

        private InteractionState interactionState = InteractionState.None;

        private Image m_FillImage;
        private Transform m_FillTransform;
        private RectTransform m_FillContainerRect;
        private Transform m_HighHandleTransform;
        private RectTransform m_HighHandleContainerRect;
        private Transform m_LowHandleTransform;
        private RectTransform m_LowHandleContainerRect;

        // The offset from interacted component position to mouse down position
        private Vector2 m_Offset = Vector2.zero;

        private DrivenRectTransformTracker m_Tracker;

        // This "delayed" mechanism is required for case 1037681.
        private bool m_DelayedUpdateVisuals = false;

        // Size of each step.
        float StepSize { get { return WholeNumbers ? 1 : (MaxValue - MinValue) * 0.1f; } }

        protected RangeSlider()
        {
        }

#if UNITY_EDITOR
        protected override void OnValidate()
        {
            base.OnValidate();

            if (WholeNumbers)
            {
                m_MinValue = Mathf.Round(m_MinValue);
                m_MaxValue = Mathf.Round(m_MaxValue);
            }

            if (IsActive())
            {
                UpdateCachedReferences();
                SetLow(m_LowValue, false);
                SetHigh(m_HighValue, false);
                //Update rects since other things might affect them even if value didn't change
                m_DelayedUpdateVisuals = true;
            }

            if (!UnityEditor.PrefabUtility.IsPartOfPrefabAsset(this) && !Application.isPlaying)
            {
                CanvasUpdateRegistry.RegisterCanvasElementForLayoutRebuild(this);
            }
        }
#endif

        public virtual void Rebuild(CanvasUpdate executing)
        {
#if UNITY_EDITOR
            if (executing == CanvasUpdate.Prelayout)
            {
                OnValueChanged.Invoke(LowValue, HighValue);
            }
#endif
        }

        /// <summary>
        /// See ICanvasElement.LayoutComplete
        /// </summary>
        public virtual void LayoutComplete()
        {
        }

        /// <summary>
        /// See ICanvasElement.GraphicUpdateComplete
        /// </summary>
        public virtual void GraphicUpdateComplete()
        {
        }

        public static bool SetClass<T>(ref T currentValue, T newValue) where T : class
        {
            if ((currentValue == null && newValue == null) || (currentValue != null && currentValue.Equals(newValue)))
                return false;

            currentValue = newValue;
            return true;
        }

        public static bool SetStruct<T>(ref T currentValue, T newValue) where T : struct
        {
            if (currentValue.Equals(newValue))
                return false;

            currentValue = newValue;
            return true;
        }

        protected override void OnEnable()
        {
            base.OnEnable();
            UpdateCachedReferences();
            SetLow(LowValue, false);
            SetHigh(HighValue, false);
            // Update rects since they need to be initialized correctly.
            UpdateVisuals();
        }

        protected override void OnDisable()
        {
            m_Tracker.Clear();
            base.OnDisable();
        }

        /// <summary>
        /// Update the rect based on the delayed update visuals.
        /// Got around issue of calling sendMessage from onValidate.
        /// </summary>
        protected virtual void Update()
        {
            if (m_DelayedUpdateVisuals)
            {
                m_DelayedUpdateVisuals = false;
                UpdateVisuals();
            }
        }

        protected override void OnDidApplyAnimationProperties()
        {
            base.OnDidApplyAnimationProperties();
        }

        void UpdateCachedReferences()
        {
            if (m_FillRect && m_FillRect != (RectTransform)transform)
            {
                m_FillTransform = m_FillRect.transform;
                m_FillImage = m_FillRect.GetComponent<Image>();
                if (m_FillTransform.parent != null)
                    m_FillContainerRect = m_FillTransform.parent.GetComponent<RectTransform>();
            }
            else
            {
                m_FillRect = null;
                m_FillContainerRect = null;
                m_FillImage = null;
            }

            if (m_HighHandleRect && m_HighHandleRect != (RectTransform)transform)
            {
                m_HighHandleTransform = m_HighHandleRect.transform;
                if (m_HighHandleTransform.parent != null)
                    m_HighHandleContainerRect = m_HighHandleTransform.parent.GetComponent<RectTransform>();
            }
            else
            {
                m_HighHandleRect = null;
                m_HighHandleContainerRect = null;
            }

            if (m_LowHandleRect && m_LowHandleRect != (RectTransform)transform)
            {
                m_LowHandleTransform = m_LowHandleRect.transform;
                if (m_LowHandleTransform.parent != null)
                {
                    m_LowHandleContainerRect = m_LowHandleTransform.parent.GetComponent<RectTransform>();
                }
            }
            else
            {
                m_LowHandleRect = null;
                m_LowHandleContainerRect = null;
            }
        }

        void SetLow(float input)
        {
            SetLow(input, true);
        }

        protected virtual void SetLow(float input, bool sendCallback)
        {
            // Clamp the input
            float newValue = Mathf.Clamp(input, MinValue, HighValue); //clamp between min and High
            if (WholeNumbers)
            {
                newValue = Mathf.Round(newValue);
            }

            // If the stepped value doesn't match the last one, it's time to update
            if (m_LowValue == newValue)
                return;

            m_LowValue = newValue;
            UpdateVisuals();
            if (sendCallback)
            {
                UISystemProfilerApi.AddMarker("RangeSlider.lowValue", this);
                m_OnValueChanged.Invoke(newValue, HighValue);
            }
        }

        void SetHigh(float input)
        {
            SetHigh(input, true);
        }

        protected virtual void SetHigh(float input, bool sendCallback)
        {
            // Clamp the input
            float newValue = Mathf.Clamp(input, LowValue, MaxValue); //clamp between min and High
            if (WholeNumbers)
            {
                newValue = Mathf.Round(newValue);
            }

            // If the stepped value doesn't match the last one, it's time to update
            if (m_HighValue == newValue)
                return;

            m_HighValue = newValue;
            UpdateVisuals();
            if (sendCallback)
            {
                UISystemProfilerApi.AddMarker("RangeSlider.highValue", this);
                m_OnValueChanged.Invoke(LowValue, newValue);
            }
        }


        protected override void OnRectTransformDimensionsChange()
        {
            base.OnRectTransformDimensionsChange();

            //This can be invoked before OnEnabled is called. So we shouldn't be accessing other objects, before OnEnable is called.
            if (!IsActive())
                return;

            UpdateVisuals();
        }

        enum Axis
        {
            Horizontal = 0,
            Vertical = 1
        }

        Axis axis { get { return m_Direction == Direction.Horizontal ? Axis.Horizontal : Axis.Vertical; } }

        // Force-update the slider. Useful if you've changed the properties and want it to update visually.
        private void UpdateVisuals()
        {
#if UNITY_EDITOR
            if (!Application.isPlaying)
                UpdateCachedReferences();
#endif

            m_Tracker.Clear();

            if (m_FillContainerRect != null)
            {
                m_Tracker.Add(this, m_FillRect, DrivenTransformProperties.Anchors);
                Vector2 anchorMin = Vector2.zero;
                Vector2 anchorMax = Vector2.one;

                //this is where some new magic must happen. Slider just uses a filled image
                //and changes the % of fill. We must move the image anchors to be between the two handles.
                anchorMin[(int)axis] = NormalizedLowValue;
                anchorMax[(int)axis] = NormalizedHighValue;

                m_FillRect.anchorMin = anchorMin;
                m_FillRect.anchorMax = anchorMax;
            }

            if (m_LowHandleContainerRect != null)
            {
                m_Tracker.Add(this, m_LowHandleRect, DrivenTransformProperties.Anchors);
                Vector2 anchorMin = Vector2.zero;
                Vector2 anchorMax = Vector2.one;
                anchorMin[(int)axis] = anchorMax[(int)axis] = NormalizedLowValue;
                m_LowHandleRect.anchorMin = anchorMin;
                m_LowHandleRect.anchorMax = anchorMax;
            }

            if (m_HighHandleContainerRect != null)
            {
                m_Tracker.Add(this, m_HighHandleRect, DrivenTransformProperties.Anchors);
                Vector2 anchorMin = Vector2.zero;
                Vector2 anchorMax = Vector2.one;
                anchorMin[(int)axis] = anchorMax[(int)axis] = NormalizedHighValue;
                m_HighHandleRect.anchorMin = anchorMin;
                m_HighHandleRect.anchorMax = anchorMax;
            }
        }

        // Update the slider's position based on the mouse.
        void UpdateDrag(PointerEventData eventData, Camera cam)
        {
            //this needs to differ from slider in that we have two handles, and need to move the right one. 
            //and if it was neither handle, we will have a separate case where both handles move uniformly 
            //moving the entire range

            //this is where we use our interationState
            switch (interactionState)
            {
                case InteractionState.Low:
                    NormalizedLowValue = CalculateDrag(eventData, cam, m_LowHandleContainerRect);
                    break;
                case InteractionState.High:
                    NormalizedHighValue = CalculateDrag(eventData, cam, m_HighHandleContainerRect);
                    break;
                case InteractionState.Bar:
                    //special case
                    CalculateBarDrag(eventData, cam);
                    break;
                case InteractionState.None:
                    break;
            }
        }

        private float CalculateDrag(PointerEventData eventData, Camera cam, RectTransform containerRect)
        {
            RectTransform clickRect = containerRect ?? m_FillContainerRect;
            if (clickRect != null && clickRect.rect.size[(int)axis] > 0)
            {
                Vector2 localCursor;
                if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(clickRect, eventData.position, cam,
                        out localCursor))
                {
                    return 0f;
                }

                localCursor -= clickRect.rect.position;

                float val = Mathf.Clamp01((localCursor - m_Offset)[(int)axis] / clickRect.rect.size[(int)axis]);

                return val;
            }

            return 0;
        }

        private void CalculateBarDrag(PointerEventData eventData, Camera cam)
        {
            RectTransform clickRect = m_FillContainerRect;
            if (clickRect != null && clickRect.rect.size[(int)axis] > 0)
            {
                Vector2 localCursor;
                if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(clickRect, eventData.position, cam,
                        out localCursor))
                {
                    return;
                }

                localCursor -= clickRect.rect.position;

                //now we need to get the delta drag on the bar
                //and move both the normalized low and high values by this amount
                //but also check that neither is going beyond the bounds
                if (NormalizedLowValue >= 0 && NormalizedHighValue <= 1)
                {
                    //find the mid point on the current bar
                    float mid = (NormalizedHighValue + NormalizedLowValue) / 2;
                    //find where the new mid point should be
                    float val = Mathf.Clamp01((localCursor - m_Offset)[(int)axis] / clickRect.rect.size[(int)axis]);
                    //calculate the delta
                    float delta = val - mid;
                    //check the clamp range
                    if (NormalizedLowValue + delta < 0)
                    {
                        delta = -NormalizedLowValue;
                    }
                    else if (NormalizedHighValue + delta > 1)
                    {
                        delta = 1 - NormalizedHighValue;
                    }

                    //adjust both ends
                    NormalizedLowValue += delta;
                    NormalizedHighValue += delta;
                }
            }
        }

        private bool MayDrag(PointerEventData eventData)
        {
            return IsActive() && IsInteractable() && eventData.button == PointerEventData.InputButton.Left;
        }

        public override void OnPointerDown(PointerEventData eventData)
        {
            if (!MayDrag(eventData))
                return;

            //HANDLE DRAG EVENTS
            m_Offset = Vector2.zero;
            if(m_LowHandleRect != null && LowValue == MaxValue && RectTransformUtility.RectangleContainsScreenPoint(m_LowHandleRect, eventData.position, eventData.enterEventCamera))
            {
                SetToMoveLowValueHandle(m_LowHandleRect, eventData);
            }
            else if (m_HighHandleRect != null && RectTransformUtility.RectangleContainsScreenPoint(m_HighHandleRect, eventData.position, eventData.enterEventCamera))
            {
                SetToMoveHighValueHandle(m_HighHandleRect, eventData);
            }
            else if (m_LowHandleRect != null && RectTransformUtility.RectangleContainsScreenPoint(m_LowHandleRect, eventData.position, eventData.enterEventCamera))
            {
                SetToMoveLowValueHandle(m_LowHandleRect, eventData);
            }
            else if (m_FillRect != null && RectTransformUtility.RectangleContainsScreenPoint(m_FillRect, eventData.position, eventData.enterEventCamera))
            {
                if (RectTransformUtility.ScreenPointToLocalPointInRectangle(m_FillRect, eventData.position, eventData.pressEventCamera, out var localMousePos))
                {
                    m_Offset = localMousePos;
                }

                interactionState = InteractionState.Bar;


                if (transition == Transition.ColorTint)
                {
                    targetGraphic = m_FillImage;
                }
            }
            else
            {
                //outside the handles, move the entire slider along
                UpdateDrag(eventData, eventData.pressEventCamera);
            }

            base.OnPointerDown(eventData);
        }

        private void SetToMoveLowValueHandle(RectTransform transform, PointerEventData eventData)
        {
            //dragging the low value handle
            if (RectTransformUtility.ScreenPointToLocalPointInRectangle(transform, eventData.position, eventData.pressEventCamera, out var localMousePos))
            {
                m_Offset = localMousePos;
            }
            interactionState = InteractionState.Low;
            if (transition == Transition.ColorTint)
            {
                targetGraphic = m_LowHandleRect.GetComponent<Graphic>();
            }
        }

        private void SetToMoveHighValueHandle(RectTransform transform, PointerEventData eventData)
        {
            //dragging the low value handle
            if (RectTransformUtility.ScreenPointToLocalPointInRectangle(transform, eventData.position, eventData.pressEventCamera, out var localMousePos))
            {
                m_Offset = localMousePos;
            }
            interactionState = InteractionState.High;
            if (transition == Transition.ColorTint)
            {
                targetGraphic = m_HighHandleRect.GetComponent<Graphic>();
            }
        }

        public virtual void OnDrag(PointerEventData eventData)
        {
            if (!MayDrag(eventData))
            {
                return;
            }

            UpdateDrag(eventData, eventData.pressEventCamera);
        }

        public override void OnPointerUp(PointerEventData eventData)
        {
            base.OnPointerUp(eventData);
            interactionState = InteractionState.None;
        }

        public override void OnMove(AxisEventData eventData)
        {
            //this requires further investigation
        }

        public virtual void OnInitializePotentialDrag(PointerEventData eventData)
        {
            eventData.useDragThreshold = false;
        }
        
        public void SetDirection(Direction direction, bool includeRectLayouts)
        {
            Axis oldAxis = axis;
            this.direction = direction;

            if (!includeRectLayouts)
                return;

            if (axis != oldAxis)
                RectTransformUtility.FlipLayoutAxes(transform as RectTransform, true, true);
        }
    }
}