//
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
//
using System.Collections.Generic;
using UnityEngine;

namespace HoloToolkit.Unity
{
    public static class RaycastHelper
    {
        public static bool DebugEnabled = false;

        public delegate bool RaycastFunc(Vector3 origin, Vector3 direction, float distance, LayerMask layerMask, out RaycastResultHelper result);

        public static bool First(Vector3 origin, Vector3 direction, float distance, LayerMask layerMask, out RaycastResultHelper result)
        {
            result = default(RaycastResultHelper);

            RaycastHit hit;
            bool hitSomething = false;

            // Check to see if the ray cast hits any of the requested Unity layers
            if (layerMask != 0 &&
                UnityEngine.Physics.Raycast(origin, direction, out hit, distance, layerMask))
            {
                result = new RaycastResultHelper(hit, layerMask);

                hitSomething = true;
            }

            return hitSomething;
        }

        public static bool SphereFirst(Vector3 origin, Vector3 direction, float radius, float distance, LayerMask layerMask, out RaycastResultHelper result)
        {
            result = default(RaycastResultHelper);

            RaycastHit hit;
            bool hitSomething = false;

            // Check to see if the ray cast hits any of the requested Unity layers
            if (layerMask != 0 &&
                UnityEngine.Physics.SphereCast(origin, radius, direction, out hit, distance, layerMask))
            {
                result = new RaycastResultHelper(hit, layerMask);

                hitSomething = true;
            }

            return hitSomething;
        }

        public static List<RaycastResultHelper> All(Vector3 origin, Vector3 direction, float distance, LayerMask layerMask)
        {
            return All(origin, direction, distance, layerMask, null);
        }

        public static List<RaycastResultHelper> All(Vector3 origin, Vector3 direction, float distance, LayerMask layerMask, List<Collider> movedColliders)
        {
            // Check to see if the ray cast hits any of the requested Unity layers
            List<RaycastResultHelper> results = null;
            if (layerMask != 0)
            {
                var raycastHits = UnityEngine.Physics.RaycastAll(origin, direction, distance, layerMask);
                if (raycastHits != null)
                {
                    results = new List<RaycastResultHelper>(raycastHits.Length);
                    foreach (var raycastHit in raycastHits)
                    {
                        results.Add(new RaycastResultHelper(raycastHit, layerMask));
                    }
                }
            }

            // If we have colliders that have moved, then remove them from the results list and redo the raycast.
            if (movedColliders != null)
            {
                RaycastHit hitInfo;
                Ray ray = new Ray(origin, direction);
                foreach (var collider in movedColliders)
                {
                    if ((collider.gameObject.layer & layerMask) != 0)
                    {
                        int colliderIndex = results.FindIndex(x => x.Collider == collider);

                        if (collider.Raycast(ray, out hitInfo, distance))
                        {
                            if (colliderIndex >= 0)
                            {
                                results[colliderIndex] = new RaycastResultHelper(hitInfo, layerMask);
                            }
                            else
                            {
                                results.Add(new RaycastResultHelper(hitInfo, layerMask));
                            }
                        }
                        else if (colliderIndex >= 0)
                        {
                            results.RemoveAt(colliderIndex);
                        }
                    }
                }
            }

            // Unity doesn't return hit results in any particular order, so we need to sort them to closest first.
            if (results != null)
            {
                results.Sort((x, y) => x.Distance < y.Distance ? -1 : 1);
            }

            return results;
        }

        public static Vector3 GetBoxColliderExtents(BoxCollider boxCollider)
        {
            return boxCollider.size;
        }

        // raysPerEdge should be odd
        public static bool CastBoxExtents(Vector3 extents, Vector3 targetPosition, Matrix4x4 trs, Ray ray, float maxDistance, LayerMask surface, RaycastFunc raycastFunc, int raysPerEdge, bool ortho, out Vector3[] points, out Vector3[] normals, out bool[] hits)
        {
            bool debugEnabled = DebugEnabled;
            if (debugEnabled)
            {
                Debug.DrawLine(ray.origin, ray.origin + ray.direction * 10.0f, Color.green);
            }

            extents /= (raysPerEdge - 1);

            int halfRaysPerEdge = (raysPerEdge - 1) / 2;
            int numRays = raysPerEdge * raysPerEdge;

            bool hitSomething = false;

            points = new Vector3[numRays];
            normals = new Vector3[numRays];
            hits = new bool[numRays];

            int index = 0;

            for (int x = -halfRaysPerEdge; x <= halfRaysPerEdge; x += 1)
            {
                for (int y = -halfRaysPerEdge; y <= halfRaysPerEdge; y += 1)
                {
                    Vector3 offset = trs.MultiplyVector(new Vector3(x * extents.x, y * extents.y, 0));

                    Vector3 origin = ray.origin;
                    Vector3 direction = (targetPosition + offset) - ray.origin;

                    if (ortho)
                    {
                        origin += offset;
                        direction = ray.direction;
                    }

                    RaycastResultHelper rayHit;
                    hits[index] = raycastFunc(origin, direction, maxDistance, surface, out rayHit);

                    if (hits[index])
                    {
                        hitSomething = true;
                        points[index] = rayHit.Point;
                        normals[index] = rayHit.Normal;

                        if (debugEnabled)
                        {
                            Debug.DrawLine(origin, points[index], Color.yellow);
                        }
                    }
                    else
                    {
                        if (debugEnabled)
                        {
                            Debug.DrawLine(origin, origin + direction * 3.0f, Color.gray);
                        }
                    }

                    index++;
                }
            }

            return hitSomething;
        }
    }
}
