/**
 * Copyright(c) Live2D Inc. All rights reserved.
 *
 * Use of this source code is governed by the Live2D Open Software license
 * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html.
 */
using Live2D.Cubism.Core;
using Live2D.Cubism.Rendering;
using System.Collections.Generic;
using UnityEngine;
namespace Live2D.Cubism.Framework.Raycasting
{
    /// 
    /// Allows casting rays against s.
    /// 
    public sealed class CubismRaycaster : MonoBehaviour
    {
        /// 
        /// s with s attached.
        /// 
        private CubismRenderer[] Raycastables { get; set; }
        /// 
        /// s with s attached.??????????????
        /// 
        private CubismRaycastablePrecision[] RaycastablePrecisions { get; set; }
        /// 
        /// Refreshes the controller. Call this method after adding and/or removing .
        /// 
        private void Refresh()
        {
            var candidates = this
                .FindCubismModel()
                .Drawables;
            // Find raycastable drawables.
            var raycastables = new List();
            var raycastablePrecisions = new List();
            for (var i = 0; i < candidates.Length; i++)
            {
                // Skip non-raycastables.
                if (candidates[i].GetComponent() == null)
                {
                    continue;
                }
                raycastables.Add(candidates[i].GetComponent());
                raycastablePrecisions.Add(candidates[i].GetComponent().Precision);
            }
            // Cache raycastables.
            Raycastables = raycastables.ToArray();
            RaycastablePrecisions = raycastablePrecisions.ToArray();
        }
        #region Unity Event Handling
        /// 
        /// Called by Unity. Makes sure cache is initialized.
        /// 
        private void Start()
        {
            // Initialize cache.
            Refresh();
        }
        #endregion
        /// 
        /// Casts a ray.
        /// 
        /// The origin of the ray.
        /// The direction of the ray.
        /// The result of the cast.
        /// [Optional] The maximum distance of the ray.
        ///  in case of a hit;  otherwise.
        /// The numbers of drawables had hit
        public int Raycast(Vector3 origin, Vector3 direction, CubismRaycastHit[] result, float maximumDistance = Mathf.Infinity)
        {
            return Raycast(new Ray(origin, direction), result, maximumDistance);
        }
        /// 
        /// Casts a ray.
        /// 
        /// 
        /// The result of the cast.
        /// [Optional] The maximum distance of the ray.
        ///  in case of a hit;  otherwise.
        /// The numbers of drawables had hit
        public int Raycast(Ray ray, CubismRaycastHit[] result, float maximumDistance = Mathf.Infinity)
        {
            // Cast ray against model plane.
            var intersectionInWorldSpace = ray.origin + ray.direction * (ray.direction.z / ray.origin.z);
            var intersectionInLocalSpace = transform.InverseTransformPoint(intersectionInWorldSpace);
            intersectionInLocalSpace.z = 0;
            var distance = intersectionInWorldSpace.magnitude;
            // Return non-hits.
            if (distance > maximumDistance)
            {
                return 0;
            }
            // Cast against each raycastable.
            var hitCount = 0;
            for (var i = 0; i < Raycastables.Length; i++)
            {
                var raycastable = Raycastables[i];
                var raycastablePrecision = RaycastablePrecisions[i];
                // Skip inactive raycastables.
                if (!raycastable.MeshRenderer.enabled)
                {
                    continue;
                }
                var bounds = raycastable.Mesh.bounds;
                // Skip non hits (bounding box)
                if (!bounds.Contains(intersectionInLocalSpace))
                {
                    continue;
                }
                // Do detailed hit-detection against mesh if requested.
                if (raycastablePrecision == CubismRaycastablePrecision.Triangles)
                {
                    if (!ContainsInTriangles(raycastable.Mesh, intersectionInLocalSpace))
                    {
                        continue;
                    }
                }
                result[hitCount].Drawable = raycastable.GetComponent();
                result[hitCount].Distance = distance;
                result[hitCount].LocalPosition = intersectionInLocalSpace;
                result[hitCount].WorldPosition = intersectionInWorldSpace;
                ++hitCount;
                // Exit if result buffer is full.
                if (hitCount == result.Length)
                {
                    break;
                }
            }
            return hitCount;
        }
        /// 
        /// Check the point is inside polygons.
        /// 
        /// 
        /// 
        /// 
        private bool ContainsInTriangles(Mesh mesh, Vector3 inputPosition)
        {
            for (var i = 0; i < mesh.triangles.Length; i+=3)
            {
                var vertexPositionA = mesh.vertices[mesh.triangles[i]];
                var vertexPositionB = mesh.vertices[mesh.triangles[i + 1]];
                var vertexPositionC = mesh.vertices[mesh.triangles[i + 2]];
                var crossProduct1 =
                    (vertexPositionB.x - vertexPositionA.x) * (inputPosition.y - vertexPositionB.y) -
                    (vertexPositionB.y - vertexPositionA.y) * (inputPosition.x - vertexPositionB.x);
                var crossProduct2 =
                    (vertexPositionC.x - vertexPositionB.x) * (inputPosition.y - vertexPositionC.y) -
                    (vertexPositionC.y - vertexPositionB.y) * (inputPosition.x - vertexPositionC.x);
                var crossProduct3 =
                    (vertexPositionA.x - vertexPositionC.x) * (inputPosition.y - vertexPositionA.y) -
                    (vertexPositionA.y - vertexPositionC.y) * (inputPosition.x - vertexPositionA.x);
                if ((crossProduct1 > 0 && crossProduct2 > 0 && crossProduct3 > 0) ||
                    (crossProduct1 < 0 && crossProduct2 < 0 && crossProduct3 < 0))
                {
                    return true;
                }
            }
            return false;
        }
    }
}