//-----------------------------------------------------------------------
// Copyright 2016 Tobii AB (publ). All rights reserved.
//-----------------------------------------------------------------------

using UnityEngine;
using System.Collections.Generic;

namespace Tobii.EyeTracking
{
    /// <summary>
    /// This Class contains some basic implementations of techniques for gaze selection for unity.
    /// </summary>
    public class HitTestFromPoint
    {
        /// <summary>
        /// Performs a hit test in World space for the given point, sets the hitInfo 
        /// out parameter and returns true if an object was found, returns false otherwise.
        /// </summary>
        /// <param name="hitInfo">Out: If true is returned, hitInfo will contain more information about where the collider was hit.</param>
        /// <param name="point">Point to hit test from.</param>
        /// <param name="camera">Camera that the point is relative to.</param>
        /// <param name="distance">Optional: Maximum distance to hit test.</param>
        /// <param name="layerMask">Optional: Which layers to hit test on.</param>
        /// <returns>True if an object was found, false otherwise.</returns>
        public static bool FindObjectInWorld(out RaycastHit hitInfo, Vector2 point, Camera camera, float distance = Mathf.Infinity, int layerMask = 1)
        {
            Ray hitTestRay = camera.ScreenPointToRay(point);
            return Physics.Raycast(hitTestRay, out hitInfo, distance, layerMask);
        }

        /// <summary>
        /// Performs hit tests on all the provided points in World space. Sets
        /// the hitInfos out parameter and returns true if at least one object
        /// was hit, returns false otherwise.
        /// </summary>
        /// <param name="hitInfos">Out: If true is returned, hitInfos will contain information about where the colliders were hit.</param>
        /// <param name="points">The points to hit test.</param>
        /// <param name="camera">Camera that the point is relative to.</param>
        /// <param name="distance">Optional: Maximum distance to hit test</param>
        /// <param name="layerMask">Optional: Which layers to hit test on.</param>
        /// <returns></returns>
        public static bool FindMultipleObjectsInWorldFromMultiplePoints(out IEnumerable<RaycastHit> hitInfos, IEnumerable<Vector2> points, Camera camera, float distance = Mathf.Infinity, int layerMask = 1)
        {
            IEnumerable<Ray> rays = CreateRaysFromPoints(points, camera);
            var isAtLeastOneHit = false;
            var hitList = new List<RaycastHit>();

            foreach (Ray ray in rays)
            {
                RaycastHit localHitInfo;
                if (Physics.Raycast(ray, out localHitInfo, distance, layerMask))
                {
                    isAtLeastOneHit = true;

                    hitList.Add(localHitInfo);
                }
            }

            hitInfos = hitList;

            return isAtLeastOneHit;
        }

        /// <summary>
        /// Performs a hit test for the given point on the <see cref="UnityEngine.Canvas"/>,
        /// sets the hitObject out parameter and returns true if an object was found, returns 
        /// false otherwise.
        /// </summary>
        /// <param name="selectedObject">Out: the game object that was selected</param>
        /// <param name="point">The point to hit test from.</param>
        /// <param name="layerMask">Optional: Which layers to hit test on.</param>
        /// <param name="minDepth">Optional: Minimum depth to hit test.</param>
        /// <param name="maxDepth">Optional: Maximum depth to hit test.</param>
        /// <returns>True if an object was found, false otherwise.</returns>
        public static bool FindObjectOnCanvas(out GameObject selectedObject, Vector2 point, int layerMask = -1, float minDepth = -Mathf.Infinity, float maxDepth = Mathf.Infinity)
        {
            Collider2D selectedCollider = Physics2D.OverlapPoint(point, layerMask, minDepth, maxDepth);
            if (selectedCollider)
            {
                selectedObject = selectedCollider.gameObject;
                return true;
            }

            selectedObject = null;
            return false;
        }

        /// <summary>
        /// Creates a collection of <see cref="Ray"/> objects from a collection
        /// of <see cref="Vector2"/> coordinates on <see cref="Screen"/>.
        /// </summary>
        /// <param name="points">The points to create rays from.</param>
        /// <param name="camera">The camera to use.</param>
        /// <returns></returns>
        private static IEnumerable<Ray> CreateRaysFromPoints(IEnumerable<Vector2> points, Camera camera)
        {
            var rays = new List<Ray>();
            foreach (var point in points)
            {
                rays.Add(camera.ScreenPointToRay(point));
            }

            return rays;
        }
    }

    public static class PatternGenerator
    {
        public static IEnumerable<Vector2> CreateCircularAreaUniformPattern(Vector2 centralPoint, int radiusInScreenPixels, int numberOfPoints)
        {
            var points = new List<Vector2>();
            for (int i = 0; i < numberOfPoints; ++i)
            {
                var point = Random.insideUnitCircle * radiusInScreenPixels + centralPoint;
                points.Add(point);
            }

            return points;
        }

        /// <summary>
        /// Creating a Circle Ray pattern arround the original Gaze Point
        /// </summary>
        /// <param name="centralPoint"></param>
        /// <param name="camera"></param>
        /// <param name="numberOfPoints"></param>
        /// <param name="patternRadius"></param>
        /// <returns></returns>
        public static Ray[] CreateCircleAroundCentralPoint(Vector2 centralPoint, Camera camera, int numberOfPoints, float patternRadius)
        {
            var positionsOfSecondRaysOnScreen = new Ray[numberOfPoints];
            var step = 0;

            for (var alpha = 0; alpha < 360; alpha += 360 / numberOfPoints)
            {
                float heading = Mathf.Deg2Rad * alpha;
                var offset = new Vector2(Mathf.Cos(heading) * patternRadius, Mathf.Sin(heading) * patternRadius);
                positionsOfSecondRaysOnScreen[step] = camera.ScreenPointToRay(centralPoint + offset);
                step++;
            }

            return positionsOfSecondRaysOnScreen;
        }
    }
}

