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

namespace UnityEngine.UI
{

[AddComponentMenu ("UI/Scroll Rect", 33)]
[SelectionBase]
[ExecuteInEditMode]
[RequireComponent(typeof(RectTransform))]
public class ScrollRect : UIBehaviour, IPopulateDragThresholdHandler, IBeginDragHandler, IEndDragHandler, IDragHandler, IScrollHandler, ICanvasElement
{
	public enum MovementType
	{
		Unrestricted, // Unrestricted movement -- can scroll forever
		Elastic, // Restricted but flexible -- can go past the edges, but springs back in place
		Clamped, // Restricted movement where it's not possible to go past the edges
	}
	
	[Serializable]
	public class ScrollRectEvent : UnityEvent<Vector2> { }
	
	[SerializeField]
	private RectTransform m_Content;
	public RectTransform content { get { return m_Content; } set { m_Content = value; } }
	
	[SerializeField]
	private bool m_Horizontal = true;
	public bool horizontal { get { return m_Horizontal; } set { m_Horizontal = value; } }
	
	[SerializeField]
	private bool m_Vertical = true;
	public bool vertical { get { return m_Vertical; } set { m_Vertical = value; } }
	
	[SerializeField]
	private MovementType m_MovementType = MovementType.Elastic;
	public MovementType movementType { get { return m_MovementType; } set { m_MovementType = value; } }
	
	[SerializeField]
	private float m_Elasticity = 0.1f; // Only used for MovementType.Elastic
	public float elasticity { get { return m_Elasticity; } set { m_Elasticity = value; } }
	
	[SerializeField]
	private bool m_Inertia = true;
	public bool inertia { get { return m_Inertia; } set { m_Inertia = value; } }
	
	[SerializeField]
	private float m_DecelerationRate = 0.135f; // Only used when inertia is enabled
	public float decelerationRate { get { return m_DecelerationRate; } set { m_DecelerationRate = value; } }
	
	[SerializeField]
	private float m_ScrollSensitivity = 1.0f;
	public float scrollSensitivity { get { return m_ScrollSensitivity; } set { m_ScrollSensitivity = value; } }
	
	[SerializeField]
	private int m_DragThreshold = 15;
	public int dragThreshold { get { return m_DragThreshold; } set { m_DragThreshold = value; } }
	
	[SerializeField]
	private Scrollbar m_HorizontalScrollbar;
	public Scrollbar horizontalScrollbar
	{
		get
		{
			return m_HorizontalScrollbar;
		}
		set
		{
			if (m_HorizontalScrollbar)
				m_HorizontalScrollbar.onValueChanged.RemoveListener (SetHorizontalNormalizedPosition);
			m_HorizontalScrollbar = value;
			if (m_HorizontalScrollbar)
				m_HorizontalScrollbar.onValueChanged.AddListener (SetHorizontalNormalizedPosition);
		}
	}
	
	[SerializeField]
	private Scrollbar m_VerticalScrollbar;
	public Scrollbar verticalScrollbar
	{
		get
		{
			return m_VerticalScrollbar;
		}
		set
		{
			if (m_VerticalScrollbar)
				m_VerticalScrollbar.onValueChanged.RemoveListener (SetVerticalNormalizedPosition);
			m_VerticalScrollbar = value;
			if (m_VerticalScrollbar)
				m_VerticalScrollbar.onValueChanged.AddListener (SetVerticalNormalizedPosition);
		}
	}
	
	[SerializeField]
	private ScrollRectEvent m_OnValueChanged = new ScrollRectEvent ();
	public ScrollRectEvent onValueChanged { get { return m_OnValueChanged; } set { m_OnValueChanged = value; } }
	
	// The offset from handle position to mouse down position
	private Vector2 m_PointerStartLocalCursor = Vector2.zero;
	private Vector2 m_ContentStartPosition = Vector2.zero;
	
	private RectTransform m_ViewRect;
	private Bounds m_ContentBounds;
	private Bounds m_ViewBounds;
	
	private Vector2 m_Velocity;
	public Vector2 velocity { get { return m_Velocity; } set { m_Velocity = value; } }
	
	private bool m_Dragging = false;
	
	private Vector2 m_PrevPosition = Vector2.zero;
	private Bounds m_PrevContentBounds = new Bounds ();
	private Bounds m_PrevViewBounds = new Bounds ();

	protected ScrollRect()
	{}
	
	public virtual void Rebuild (CanvasUpdate executing)
	{
		if (executing != CanvasUpdate.PostLayout)
			return;

		UpdateBounds ();
		UpdateScrollbars (Vector2.zero);
		UpdatePrevData ();
	}
	
	protected override void OnEnable ()
	{
		base.OnEnable ();
		
		m_ViewRect = transform as RectTransform;
		
		if (m_HorizontalScrollbar)
			m_HorizontalScrollbar.onValueChanged.AddListener (SetHorizontalNormalizedPosition);
		if (m_VerticalScrollbar)
			m_VerticalScrollbar.onValueChanged.AddListener (SetVerticalNormalizedPosition);
		
		// Note: We need to ensure that the layout calculations have run before UpdateBounds is called.
		// CanvasUpdateRegistry.RegisterCanvasElementForLayoutRebuild would work for this
		// if we only called UpdateBounds outselves from inside this class.
		// Unfortunate it's also being invoked by the OnEnable and OnValidate callbacks of Scrollbar,
		// so it may well be called before the layout code has had a chance to run naturally.
		// Calling Canvas.ForceUpdateCanvases seems to fix the issue though theoretically it might not always work,
		// if certain layout elements hadn't set themselves as dirty before the ForceUpdateCanvases method is run.
		// Ideally we should find a more robust method, but it seems tricky without removing validation logic in
		// OnEnable and OnValidate of other components.
		Canvas.ForceUpdateCanvases ();
		CanvasUpdateRegistry.RegisterCanvasElementForLayoutRebuild (this);
	}
	
	protected override void OnDisable ()
	{
		base.OnDisable ();
		
		if (m_HorizontalScrollbar)
			m_HorizontalScrollbar.onValueChanged.RemoveListener (SetHorizontalNormalizedPosition);
		if (m_VerticalScrollbar)
			m_VerticalScrollbar.onValueChanged.RemoveListener (SetVerticalNormalizedPosition);
	}

	public override bool IsActive ()
	{
		return base.IsActive () && m_Content != null;
	}

	public void StopMovement()
	{
		m_Velocity = Vector2.zero;
	}
	
	public void OnScroll (PointerEventData data)
	{
		if (!IsActive ())
			return;
		
		Vector2 delta = data.scrollDelta;
		// Down is positive for scroll events, while in UI system up is positive.
		delta.y *= -1;
		if (vertical && !horizontal)
		{
			if (Mathf.Abs (delta.x) > Mathf.Abs (delta.y))
				delta.y = delta.x;
			delta.x = 0;
		}
		if (horizontal && !vertical)
		{
			if (Mathf.Abs (delta.y) > Mathf.Abs (delta.x))
				delta.x = delta.y;
			delta.y = 0;
		}
		
		Vector2 position = m_Content.anchoredPosition;
		position += delta * m_ScrollSensitivity;
		m_Content.anchoredPosition = position;
		
		UpdateBounds ();
	}

	public void OnPopulateDragThreshold(PointerEventData eventData)
	{
		eventData.dragThreshold = dragThreshold;
		m_Velocity = Vector2.zero;
	}

	public void OnBeginDrag (PointerEventData eventData) 
	{
		if (eventData.button != PointerEventData.InputButton.Left)
			return;
	
		if (!IsActive ())
			return;
		
		m_PointerStartLocalCursor = Vector2.zero;
		RectTransformUtility.ScreenPointToLocalPointInRectangle (m_ViewRect, eventData.position, eventData.pressEventCamera, out m_PointerStartLocalCursor);
		m_ContentStartPosition = m_Content.anchoredPosition;
		m_Dragging = true;
		
		UpdateBounds ();
	}

	public void OnEndDrag(PointerEventData eventData)
	{
		if (eventData.button != PointerEventData.InputButton.Left)
			return;
	
		m_Dragging = false;
	}

	public void OnDrag(PointerEventData eventData)
	{
		if (eventData.button != PointerEventData.InputButton.Left)
			return;

		if (!IsActive())
			return;
		
		Vector2 localCursor;
		if (!RectTransformUtility.ScreenPointToLocalPointInRectangle (m_ViewRect, eventData.position, eventData.pressEventCamera, out localCursor))
			return;
		
		UpdateBounds ();
		
		var pointerDelta = localCursor - m_PointerStartLocalCursor;
		Vector2 position = m_ContentStartPosition + pointerDelta;
		
		// Offset to get content into place in the view.
		Vector2 offset = CalculateOffset (position - m_Content.anchoredPosition);
		position += offset;
		if (m_MovementType == MovementType.Elastic)
		{
			if (offset.x != 0)
				position.x = position.x - RubberDelta (offset.x, m_ViewBounds.size.x);
			if (offset.y != 0)
				position.y = position.y - RubberDelta (offset.y, m_ViewBounds.size.y);
		}
		
		if (!m_Horizontal)
			position.x = m_Content.anchoredPosition.x;
		if (!m_Vertical)
			position.y = m_Content.anchoredPosition.y;
		
		m_Content.anchoredPosition = position;
	}

	protected virtual void LateUpdate ()
	{
		if (!m_Content)
			return;
		
		float deltaTime = Time.unscaledDeltaTime;
		Vector2 offset = CalculateOffset (Vector2.zero);
		if (!m_Dragging && (offset != Vector2.zero || m_Velocity != Vector2.zero))
		{
			Vector2 position = m_Content.anchoredPosition;
			for (int axis = 0; axis < 2; axis++)
			{
				// Apply spring physics if movement is elastic and content has an offset from the view.
				if (m_MovementType == MovementType.Elastic && offset[axis] != 0)
				{
					float speed = m_Velocity[axis];
					position[axis] = Mathf.SmoothDamp (m_Content.anchoredPosition[axis], m_Content.anchoredPosition[axis] + offset[axis], ref speed, m_Elasticity, Mathf.Infinity, deltaTime);
					m_Velocity[axis] = speed;
				}
				// Else move content according to velocity with deceleration applied.
				else if (m_Inertia)
				{
					m_Velocity[axis] *= Mathf.Pow (m_DecelerationRate, deltaTime);
					if (Mathf.Abs (m_Velocity[axis]) < 1)
						m_Velocity[axis] = 0;
					position[axis] += m_Velocity[axis] * deltaTime;
				}
				// If we have neither elaticity or friction, there shouldn't be any velocity.
				else
				{
					m_Velocity[axis] = 0;
				}
			}
			
			if (m_Velocity != Vector2.zero)
			{
				if (m_MovementType == MovementType.Clamped)
				{
					offset = CalculateOffset (position - m_Content.anchoredPosition);
					position += offset;
				}
				
				if (!m_Horizontal)
					position.x = m_Content.anchoredPosition.x;
				if (!m_Vertical)
					position.y = m_Content.anchoredPosition.y;
				
				if (position != m_Content.anchoredPosition)
					m_Content.anchoredPosition = position;
			}
		}
		
		if (m_Dragging && m_Inertia)
		{
			Vector3 newVelocity = (m_Content.anchoredPosition - m_PrevPosition) / deltaTime;
			m_Velocity = Vector3.Lerp (m_Velocity, newVelocity, deltaTime * 10);
		}
		
		UpdateBounds ();
		if (m_ViewBounds != m_PrevViewBounds || m_ContentBounds != m_PrevContentBounds || m_Content.anchoredPosition != m_PrevPosition)
		{
			UpdateScrollbars (offset);
			m_OnValueChanged.Invoke (normalizedPosition);
			UpdatePrevData ();
		}
	}
	
	void UpdatePrevData ()
	{
		if (m_Content == null)
			m_PrevPosition = Vector2.zero;
		else
			m_PrevPosition = m_Content.anchoredPosition;	
		m_PrevViewBounds = m_ViewBounds;
		m_PrevContentBounds = m_ContentBounds;
	}
	
	void UpdateScrollbars (Vector2 offset)
	{
		if (m_HorizontalScrollbar)
		{
			m_HorizontalScrollbar.size = Mathf.Clamp01 ((m_ViewBounds.size.x - Mathf.Abs (offset.x)) / m_ContentBounds.size.x);
			m_HorizontalScrollbar.value = horizontalNormalizedPosition;
		}
		
		if (m_VerticalScrollbar)
		{
			m_VerticalScrollbar.size = Mathf.Clamp01 ((m_ViewBounds.size.y - Mathf.Abs (offset.y)) / m_ContentBounds.size.y);
			m_VerticalScrollbar.value = verticalNormalizedPosition;
		}
	}
	
	public Vector2 normalizedPosition
	{
		get
		{
			return new Vector2 (horizontalNormalizedPosition, verticalNormalizedPosition);
		}
		set
		{
			SetHorizontalNormalizedPosition (value.x);
			SetVerticalNormalizedPosition (value.y);
		}
	}
	
	public float horizontalNormalizedPosition
	{
		get
		{
			if (m_ContentBounds.size.x <= m_ViewBounds.size.x)
				return (m_ViewBounds.min.x > m_ContentBounds.min.x) ? 1 : 0;
			return Mathf.Clamp01 ((m_ViewBounds.min.x - m_ContentBounds.min.x) / (m_ContentBounds.size.x - m_ViewBounds.size.x));
		}
		set
		{
			SetHorizontalNormalizedPosition (value);
		}
	}
	
	public float verticalNormalizedPosition
	{
		get
		{
			if (m_ContentBounds.size.y <= m_ViewBounds.size.y)
				return (m_ViewBounds.min.y > m_ContentBounds.min.y) ? 1 : 0;;
			return Mathf.Clamp01 ((m_ViewBounds.min.y - m_ContentBounds.min.y) / (m_ContentBounds.size.y - m_ViewBounds.size.y));
		}
		set
		{
			SetVerticalNormalizedPosition (value);
		}
	}
	
	void SetHorizontalNormalizedPosition (float value)
	{
		UpdateBounds ();
		float scroll = m_ViewBounds.min.x - value * (m_ContentBounds.size.x - m_ViewBounds.size.x);
		Vector2 anchoredPosition = m_Content.anchoredPosition;
		anchoredPosition.x += scroll - m_ContentBounds.min.x;
		if (Mathf.Abs (m_Content.anchoredPosition.x - anchoredPosition.x) > 0.01f)
		{
			m_Content.anchoredPosition = anchoredPosition;
			m_Velocity.x = 0;
		}
	}
	
	void SetVerticalNormalizedPosition (float value)
	{
		UpdateBounds ();
		float scroll = m_ViewBounds.min.y - value * (m_ContentBounds.size.y - m_ViewBounds.size.y);
		Vector2 anchoredPosition = m_Content.anchoredPosition;
		anchoredPosition.y += scroll - m_ContentBounds.min.y;
		if (Mathf.Abs (m_Content.anchoredPosition.y - anchoredPosition.y) > 0.01f)
		{
			m_Content.anchoredPosition = anchoredPosition;
			m_Velocity.y = 0;
		}
	}
	
	static float RubberDelta (float overStretching, float viewSize)
	{
		return (1 - (1 / ((Mathf.Abs (overStretching) * 0.55f / viewSize) + 1))) * viewSize * Mathf.Sign (overStretching);
	}

	void UpdateBounds ()
	{
		m_ViewBounds = new Bounds (m_ViewRect.rect.center, m_ViewRect.rect.size);
		m_ContentBounds = GetBounds();

		if (m_Content == null)
			return;
		
		// Make sure content bounds are at laeast as large as view by adding padding if not.
		// One might think at first that if the content is smaller than the view, scrolling should be allowed.
		// However, that's not how scroll views normally work.
		// Scrolling is *only* possible when content is *larger* than view.
		// We use the pivot of the content rect to decide in which directions the content bounds should be expanded.
		// E.g. if pivot is at top, bounds are expanded downwards.
		// This also works nicely when ContentSizeFitter is used on the content. 
		Vector3 contentSize = m_ContentBounds.size;
		Vector3 contentPos = m_ContentBounds.center;
		Vector3 excess = m_ViewBounds.size - contentSize;
		if (horizontal && excess.x > 0)
		{
			contentPos.x -= excess.x * (m_Content.pivot.x - 0.5f);
			contentSize.x = m_ViewBounds.size.x;
		}
		if (vertical && excess.y > 0)
		{
			contentPos.y -= excess.y * (m_Content.pivot.y - 0.5f);
			contentSize.y = m_ViewBounds.size.y;
		}
		
		m_ContentBounds.size = contentSize;
		m_ContentBounds.center = contentPos;
	}

	private readonly Vector3[] m_Corners = new Vector3[4]; 
	private Bounds GetBounds()
	{
		if (m_Content == null)
			return new Bounds();

		var vMin = new Vector3 (float.MaxValue, float.MaxValue, float.MaxValue);
		var vMax = new Vector3 (float.MinValue, float.MinValue, float.MinValue);

		var toLocal = m_ViewRect.worldToLocalMatrix;
		m_Content.GetWorldCorners (m_Corners);
		for (int j = 0; j < 4; j++)
		{
			Vector3 v = toLocal.MultiplyPoint3x4 (m_Corners[j]);
			vMin = Vector3.Min (v, vMin);
			vMax = Vector3.Max (v, vMax);
		}

		var bounds = new Bounds (vMin, Vector3.zero);
		bounds.Encapsulate (vMax);
		return bounds;
	}

	Vector3 CalculateOffset (Vector2 delta)
	{
		Vector3 offset = Vector3.zero;
		if (m_MovementType == MovementType.Unrestricted)
			return offset;
		
		Vector2 min = m_ContentBounds.min;
		Vector2 max = m_ContentBounds.max;
		
		if (m_Horizontal)
		{
			min.x += delta.x;
			max.x += delta.x;
			if (min.x > m_ViewBounds.min.x)
				offset.x = m_ViewBounds.min.x - min.x;
			else if (max.x < m_ViewBounds.max.x)
				offset.x = m_ViewBounds.max.x - max.x;
		}
		
		if (m_Vertical)
		{
			min.y += delta.y;
			max.y += delta.y;
			if (max.y < m_ViewBounds.max.y)
				offset.y = m_ViewBounds.max.y - max.y;
			else if (min.y > m_ViewBounds.min.y)
				offset.y = m_ViewBounds.min.y - min.y;
		}
		
		return offset;
	}
}

}