///Original Credit Korindian
///Sourced from - http://forum.unity3d.com/threads/rts-style-drag-selection-box.265739/
///Updated Credit BenZed
///Sourced from - http://forum.unity3d.com/threads/color-picker.267043/

/*
 * What the SelectionBox component does is allow the game player to select objects using an RTS style click and drag interface:
 * 
 * We want to be able to select Game Objects of any type,
 * We want to be able to drag select a group of game objects to select them,
 * We want to be able to hold the shift key and drag select a group of game objects to add them to the current selection,
 * We want to be able to single click a game object to select it,
 * We want to be able to hold the shift key and single click a game object to add it to the current selection, 
 * We want to be able to hold the shift key and single click an already selected game object to remove it from the current selection.
 * 
 * Most importantly, we want this behaviour to work with UI, 2D or 3D gameObjects, so it has to be smart about considering their respective screen spaces.
 * 
 * Add this component to a Gameobject with a Canvas with RenderMode.ScreenSpaceOverlay
 * And implement the IBoxSelectable interface on any MonoBehaviour to make it selectable.
 * 
 * Improvements that could be made:
 * 
 * Control clicking a game object to select all objects of that type or tag.
 * Compatibility with Canvas Scaling
 * Filtering single click selections of objects occupying the same space. (So that, for example, you're only click selecting the game object found closest to the camera)
 * 
 */

using System.Collections.Generic;
using UnityEngine.Events;

namespace UnityEngine.UI.Extensions
{
    [RequireComponent(typeof(Canvas))]
	[AddComponentMenu("UI/Extensions/Selection Box")]
	public class SelectionBox : MonoBehaviour
	{
		
		// The color of the selection box.
		public Color color;
		
		// An optional parameter, but you can add a sprite to the selection box to give it a border or a stylized look.
		// It's suggested you use a monochrome sprite so that the selection
		// Box color is still relevent.
		public Sprite art;
		
		// Will store the location of wherever we first click before dragging.
		private Vector2 origin;
		
		// A rectTransform set by the User that can limit which part of the screen is eligable for drag selection
		public RectTransform selectionMask;
		
		//Stores the rectTransform connected to the generated gameObject being used for the selection box visuals
		private RectTransform boxRect;
		
		// Stores all of the selectable game objects
		private IBoxSelectable[] selectables;
		
		// A secondary storage of objects that the user can manually set.
		private MonoBehaviour[] selectableGroup;
		
		//Stores the selectable that was touched when the mouse button was pressed down
		private IBoxSelectable clickedBeforeDrag;
		
		//Stores the selectable that was touched when the mouse button was released
		private IBoxSelectable clickedAfterDrag;
		
		//Custom UnityEvent so we can add Listeners to this instance when Selections are changed.
		public class SelectionEvent : UnityEvent<IBoxSelectable[]> {}
		public SelectionEvent onSelectionChange = new SelectionEvent();
		
		//Ensures that the canvas that this component is attached to is set to the correct render mode. If not, it will not render the selection box properly.
		void ValidateCanvas(){
			var canvas = gameObject.GetComponent<Canvas>();
			
			if (canvas.renderMode != RenderMode.ScreenSpaceOverlay) {
				throw new System.Exception("SelectionBox component must be placed on a canvas in Screen Space Overlay mode.");
			}
			
			var canvasScaler = gameObject.GetComponent<CanvasScaler>();
			
			if (canvasScaler && canvasScaler.enabled && (!Mathf.Approximately(canvasScaler.scaleFactor, 1f) || canvasScaler.uiScaleMode != CanvasScaler.ScaleMode.ConstantPixelSize)) {
				Destroy(canvasScaler);
				Debug.LogWarning("SelectionBox component is on a gameObject with a Canvas Scaler component. As of now, Canvas Scalers without the default settings throw off the coordinates of the selection box. Canvas Scaler has been removed.");
			}
		}
		
		/*
	 * The user can manually set a group of objects with monoBehaviours to be the pool of objects considered to be selectable. The benefits of this are two fold:
	 *
	 * 1) The default behaviour is to check every game object in the scene, which is much slower.
	 * 2) The user can filter which objects should be selectable, for example units versus menu selections
	 *
	 */
		void SetSelectableGroup(IEnumerable<MonoBehaviour> behaviourCollection) {
			
			// If null, the selectionbox reverts to it's default behaviour
			if (behaviourCollection == null) {
				selectableGroup = null;
				
				return;
			}
			
			//Runs a double check to ensure each of the objects in the collection can be selectable, and doesn't include them if not.
			var behaviourList = new List<MonoBehaviour>();
			
			foreach(var behaviour in behaviourCollection) {
				if (behaviour as IBoxSelectable != null) {
					behaviourList.Add (behaviour);
				}
			}
			
			selectableGroup = behaviourList.ToArray();
		}
		
		void CreateBoxRect(){
			var selectionBoxGO = new GameObject();
			
			selectionBoxGO.name = "Selection Box";
			selectionBoxGO.transform.parent = transform;
			selectionBoxGO.AddComponent<Image>();
			
			boxRect = selectionBoxGO.transform as RectTransform;
			
		}
		
		//Set all of the relevant rectTransform properties to zero, 
		//finally deactivates the boxRect gameobject since it doesn't
		//need to be enabled when not in a selection action.
		void ResetBoxRect(){
			
			//Update the art and color on the off chance they've changed
			Image image = boxRect.GetComponent<Image>();
			image.color = color;
			image.sprite = art;
			
			origin = Vector2.zero;
			
			boxRect.anchoredPosition = Vector2.zero;
			boxRect.sizeDelta = Vector2.zero;
			boxRect.anchorMax = Vector2.zero;
			boxRect.anchorMin = Vector2.zero;
			boxRect.pivot = Vector2.zero;
			boxRect.gameObject.SetActive(false);
		}
		
		
		void BeginSelection(){
			// Click somewhere in the Game View.
			if (!Input.GetMouseButtonDown(0))
				return;
			
			//The boxRect will be inactive up until the point we start selecting
			boxRect.gameObject.SetActive(true);
			
			// Get the initial click position of the mouse. 
			origin = new Vector2(Input.mousePosition.x, Input.mousePosition.y);
			
			//If the initial click point is not inside the selection mask, we abort the selection
			if (!PointIsValidAgainstSelectionMask(origin)) {
				ResetBoxRect();
				return;
			}
			
			// The anchor is set to the same place.
			boxRect.anchoredPosition = origin;
			
			MonoBehaviour[] behavioursToGetSelectionsFrom;
			
			// If we do not have a group of selectables already set, we'll just loop through every object that's a monobehaviour, and look for selectable interfaces in them
			if (selectableGroup == null) {
				behavioursToGetSelectionsFrom = GameObject.FindObjectsOfType<MonoBehaviour>();
			} else {
				behavioursToGetSelectionsFrom = selectableGroup;
			}
			
			//Temporary list to store the found selectables before converting to the main selectables array
			List<IBoxSelectable> selectableList = new List<IBoxSelectable>();
			
			foreach (MonoBehaviour behaviour in behavioursToGetSelectionsFrom) {
				
				//If the behaviour implements the selectable interface, we add it to the selectable list
				IBoxSelectable selectable = behaviour as IBoxSelectable;
				if (selectable != null) {
					selectableList.Add (selectable); 
					
					//We're using left shift to act as the "Add To Selection" command. So if left shift isn't pressed, we want everything to begin deselected
					if (!Input.GetKey (KeyCode.LeftShift)) {
						selectable.selected = false;
					}
				}
				
			}
			selectables = selectableList.ToArray();
			
			//For single-click actions, we need to get the selectable that was clicked when selection began (if any)
			clickedBeforeDrag = GetSelectableAtMousePosition();
			
		}
		
		bool PointIsValidAgainstSelectionMask(Vector2 screenPoint){
			//If there is no seleciton mask, any point is valid
			if (!selectionMask) {
				return true;
			}
			
			Camera screenPointCamera = GetScreenPointCamera(selectionMask);
			
			return RectTransformUtility.RectangleContainsScreenPoint(selectionMask, screenPoint, screenPointCamera);
		}
		
		IBoxSelectable GetSelectableAtMousePosition() {
			//Firstly, we cannot click on something that is not inside the selection mask (if we have one)
			if (!PointIsValidAgainstSelectionMask(Input.mousePosition)) {
				return null;
			}
			
			//This gets a bit tricky, because we have to make considerations depending on the heirarchy of the selectable's gameObject
			foreach (var selectable in selectables) {
				
				//First we check to see if the selectable has a rectTransform
				var rectTransform = (selectable.transform as RectTransform);
				
				if (rectTransform) {
					//Because if it does, the camera we use to calulate it's screen point will vary
					var screenCamera = GetScreenPointCamera(rectTransform);
					
					//Once we've found the rendering camera, we check if the selectables rectTransform contains the click. That way we
					//Can click anywhere on a rectTransform to select it.
					if (RectTransformUtility.RectangleContainsScreenPoint(rectTransform, Input.mousePosition, screenCamera)) {
						
						//And if it does, we select it and send it back
						return selectable;
					}
				} else {
					//If it doesn't have a rectTransform, we need to get the radius so we can use it as an area around the center to detect a click.
					//This works because a 2D or 3D renderer will both return a radius
					var radius = selectable.transform.GetComponent<UnityEngine.Renderer>().bounds.extents.magnitude;
					
					var selectableScreenPoint = GetScreenPointOfSelectable(selectable);
					
					//Check that the click fits within the screen-radius of the selectable
					if (Vector2.Distance(selectableScreenPoint, Input.mousePosition) <= radius) {
						
						//And if it does, we select it and send it back
						return selectable;
					}
					
				}
			}
			
			return null;
		}
		
		
		void DragSelection(){
			//Return if we're not dragging or if the selection has been aborted (BoxRect disabled)
			if (!Input.GetMouseButton(0) || !boxRect.gameObject.activeSelf)
				return;
			
			// Store the current mouse position in screen space.
			Vector2 currentMousePosition = new Vector2(Input.mousePosition.x, Input.mousePosition.y);
			
			// How far have we moved the mouse?
			Vector2 difference = currentMousePosition - origin;
			
			// Copy the initial click position to a new variable. Using the original variable will cause
			// the anchor to move around to wherever the current mouse position is,
			// which isn't desirable.
			Vector2 startPoint = origin;
			
			// The following code accounts for dragging in various directions.
			if (difference.x < 0)
			{
				startPoint.x = currentMousePosition.x;
				difference.x = -difference.x;
			}
			if (difference.y < 0)
			{
				startPoint.y = currentMousePosition.y;
				difference.y = -difference.y;
			}
			
			// Set the anchor, width and height every frame.
			boxRect.anchoredPosition = startPoint;
			boxRect.sizeDelta = difference;
			
			//Then we check our list of Selectables to see if they're being preselected or not.
			foreach(var selectable in selectables) {
				
				Vector3 screenPoint = GetScreenPointOfSelectable(selectable);
				
				//If the box Rect contains the selectabels screen point and that point is inside a valid selection mask, it's being preselected, otherwise it is not.
				selectable.preSelected = RectTransformUtility.RectangleContainsScreenPoint(boxRect, screenPoint, null) && PointIsValidAgainstSelectionMask(screenPoint);
				
			}
			
			//Finally, since it's possible for our first clicked object to not be within the bounds of the selection box
			//If it exists, we always ensure that it is preselected.
			if (clickedBeforeDrag != null) {
				clickedBeforeDrag.preSelected = true;
			}
		}
		
		void ApplySingleClickDeselection(){
			
			//If we didn't touch anything with the original mouse press, we don't need to continue checking
			if (clickedBeforeDrag == null)
				return;
			
			//If we clicked a selectable without dragging, and that selectable was previously selected, we must be trying to deselect it.
			if (clickedAfterDrag != null && clickedBeforeDrag.selected && clickedBeforeDrag.transform == clickedAfterDrag.transform ) {
				clickedBeforeDrag.selected = false;
				clickedBeforeDrag.preSelected = false;
				
			}
			
		}
		
		void ApplyPreSelections(){
			
			foreach(var selectable in selectables) {
				
				//If the selectable was preSelected, we finalize it as selected.
				if (selectable.preSelected) {
					selectable.selected = true;
					selectable.preSelected = false;
				}
			}
			
		}
		
		Vector2 GetScreenPointOfSelectable(IBoxSelectable selectable) {
			//Getting the screen point requires it's own function, because we have to take into consideration the selectables heirarchy.
			
			//Cast the transform as a rectTransform
			var rectTransform = selectable.transform as RectTransform;
			
			//If it has a rectTransform component, it must be in the heirarchy of a canvas, somewhere.
			if (rectTransform) {
				
				//And the camera used to calculate it's screen point will vary.
				Camera renderingCamera = GetScreenPointCamera(rectTransform);
				
				return RectTransformUtility.WorldToScreenPoint(renderingCamera, selectable.transform.position);
			}
			
			//If it's no in the heirarchy of a canvas, the regular Camera.main.WorldToScreenPoint will do.
			return Camera.main.WorldToScreenPoint(selectable.transform.position);				                                         
			
		}
		
		/*
	 * Finding the camera used to calculate the screenPoint of an object causes a couple of problems:
	 * 
	 * If it has a rectTransform, the root Canvas that the rectTransform is a descendant of will give unusable
	 * screen points depending on the Canvas.RenderMode, if we don't do any further calculation.
	 * 
	 * This function solves that problem. 
	 */
		Camera GetScreenPointCamera(RectTransform rectTransform) {
			
			Canvas rootCanvas = null;
			RectTransform rectCheck = rectTransform;
			
			//We're going to check all the canvases in the heirarchy of this rectTransform until we find the root.
			do {
				rootCanvas = rectCheck.GetComponent<Canvas>();
				
				//If we found a canvas on this Object, and it's not the rootCanvas, then we don't want to keep it
				if (rootCanvas && !rootCanvas.isRootCanvas) {
					rootCanvas = null;
				}
				
				//Then we promote the rect we're checking to it's parent.
				rectCheck = (RectTransform)rectCheck.parent;
				
			} while (rootCanvas == null);
			
			//Once we've found the root Canvas, we return a camera depending on it's render mode.
			switch (rootCanvas.renderMode) {
			case RenderMode.ScreenSpaceOverlay:
				//If we send back a camera when set to screen space overlay, the coordinates will not be accurate. If we return null, they will be.
				return null;
				
			case RenderMode.ScreenSpaceCamera:
				//If it's set to screen space we use the world Camera that the Canvas is using.
				//If it doesn't have one set, however, we have to send back the current camera. otherwise the coordinates will not be accurate.
				return (rootCanvas.worldCamera) ? rootCanvas.worldCamera : Camera.main;
				
			default:
			case RenderMode.WorldSpace:
				//World space always uses the current camera.
				return Camera.main;
			}
			
		}
		
		public IBoxSelectable[] GetAllSelected(){
			if (selectables == null) {
				return new IBoxSelectable[0];
			}
			
			var selectedList = new List<IBoxSelectable>();
			
			foreach(var selectable in selectables) {
				if (selectable.selected) {
					selectedList.Add (selectable);
				}
			}
			
			return selectedList.ToArray();
		}
		
		void EndSelection(){
			//Get out if we haven't finished selecting, or if the selection has been aborted (boxRect disabled)
			if (!Input.GetMouseButtonUp(0) || !boxRect.gameObject.activeSelf)
				return;
			
			clickedAfterDrag = GetSelectableAtMousePosition();
			
			ApplySingleClickDeselection();
			ApplyPreSelections();
			ResetBoxRect();
			onSelectionChange.Invoke(GetAllSelected());
		}
		
		void Start(){
			ValidateCanvas();
			CreateBoxRect();
			ResetBoxRect();
		}
		
		void Update() {
			BeginSelection ();
			DragSelection ();
			EndSelection ();
		}
	}
}
