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

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

namespace HoloToolkit.Unity.SpatialMapping
{
    /// <summary>
    /// RemoveSurfaceVertices will remove any vertices from the Spatial Mapping Mesh that fall within the bounding volume.
    /// This can be used to create holes in the environment, or to help reduce triangle count after finding planes.
    /// </summary>
    public class RemoveSurfaceVertices : Singleton<RemoveSurfaceVertices>
    {
        [Tooltip("The amount, if any, to expand each bounding volume by.")]
        public float BoundsExpansion = 0.0f;

        /// <summary>
        /// Delegate which is called when the RemoveVerticesComplete event is triggered.
        /// </summary>
        /// <param name="source"></param>
        /// <param name="args"></param>
        public delegate void EventHandler(object source, EventArgs args);

        /// <summary>
        /// EventHandler which is triggered when the RemoveSurfaceVertices is finished.
        /// </summary>
        public event EventHandler RemoveVerticesComplete;

        /// <summary>
        /// Indicates if RemoveSurfaceVertices is currently removing vertices from the Spatial Mapping Mesh.
        /// </summary>
        private bool removingVerts = false;

        /// <summary>
        /// Queue of bounding objects to remove surface vertices from.
        /// Bounding objects are queued so that RemoveSurfaceVerticesWithinBounds can be called even when the previous task has not finished.
        /// </summary>
        private Queue<Bounds> boundingObjectsQueue;

#if UNITY_EDITOR || UNITY_STANDALONE
        /// <summary>
        /// How much time (in sec), while running in the Unity Editor, to allow RemoveSurfaceVertices to consume before returning control to the main program.
        /// </summary>
        private static readonly float FrameTime = .016f;
#else
        /// <summary>
        /// How much time (in sec) to allow RemoveSurfaceVertices to consume before returning control to the main program.
        /// </summary>
        private static readonly float FrameTime = .008f;
#endif

        // GameObject initialization.
        private void Start()
        {
            boundingObjectsQueue = new Queue<Bounds>();
            removingVerts = false;
        }

        /// <summary>
        /// Removes portions of the surface mesh that exist within the bounds of the boundingObjects.
        /// </summary>
        /// <param name="boundingObjects">Collection of GameObjects that define the bounds where spatial mesh vertices should be removed.</param>
        public void RemoveSurfaceVerticesWithinBounds(IEnumerable<GameObject> boundingObjects)
        {
            if (boundingObjects == null)
            {
                return;
            }

            if (!removingVerts)
            {
                removingVerts = true;
                AddBoundingObjectsToQueue(boundingObjects);

                // We use Coroutine to split the work across multiple frames and avoid impacting the frame rate too much.
                StartCoroutine(RemoveSurfaceVerticesWithinBoundsRoutine());
            }
            else
            {
                // Add new boundingObjects to end of queue.
                AddBoundingObjectsToQueue(boundingObjects);
            }
        }

        /// <summary>
        /// Adds new bounding objects to the end of the Queue.
        /// </summary>
        /// <param name="boundingObjects">Collection of GameObjects which define the bounds where spatial mesh vertices should be removed.</param>
        private void AddBoundingObjectsToQueue(IEnumerable<GameObject> boundingObjects)
        {
            foreach (GameObject item in boundingObjects)
            {
                Collider boundingCollider = item.GetComponent<Collider>();
                if (boundingCollider != null)
                {
                    Bounds bounds = boundingCollider.bounds;

                    // Expand the bounds, if requested.
                    if (BoundsExpansion > 0.0f)
                    {
                        bounds.Expand(BoundsExpansion);
                    }

                    boundingObjectsQueue.Enqueue(bounds);
                }
            }
        }

        /// <summary>
        /// Iterator block, analyzes surface meshes to find vertices existing within the bounds of any boundingObject and removes them.
        /// </summary>
        /// <returns>Yield result.</returns>
        private IEnumerator RemoveSurfaceVerticesWithinBoundsRoutine()
        {
            List<MeshFilter> meshFilters = SpatialMappingManager.Instance.GetMeshFilters();
            float start = Time.realtimeSinceStartup;

            while (boundingObjectsQueue.Count > 0)
            {
                // Get the current boundingObject.
                Bounds bounds = boundingObjectsQueue.Dequeue();

                foreach (MeshFilter filter in meshFilters)
                {
                    // Since this is amortized across frames, the filter can be destroyed by the time
                    // we get here.
                    if (filter == null)
                    {
                        continue;
                    }

                    Mesh mesh = filter.sharedMesh;
                    MeshRenderer meshRenderer = filter.GetComponent<MeshRenderer>();
                    
                    // The mesh renderer bounds are in world space.
                    // If the mesh is null there is nothing to process
                    // If the renderer is null we can't get the renderer bounds
                    // If the renderer's bounds aren't contained inside of the current
                    // bounds from the bounds queue there is no reason to process
                    // If any of the above conditions are met, then we should go to the next meshfilter. 
                    if (mesh == null || meshRenderer == null || !meshRenderer.bounds.Intersects(bounds))
                    {
                        // We don't need to do anything to this mesh, move to the next one.
                        continue;
                    }

                    // Remove vertices from any mesh that intersects with the bounds.
                    Vector3[] verts = mesh.vertices;
                    HashSet<int> vertsToRemove = new HashSet<int>();

                    // Find which mesh vertices are within the bounds.
                    for (int i = 0; i < verts.Length; ++i)
                    {
                        if (bounds.Contains(filter.transform.TransformPoint(verts[i])))
                        {
                            // These vertices are within bounds, so mark them for removal.
                            vertsToRemove.Add(i);
                        }

                        // If too much time has passed, we need to return control to the main game loop.
                        if ((Time.realtimeSinceStartup - start) > FrameTime)
                        {
                            // Pause our work here, and continue finding vertices to remove on the next frame.
                            yield return null;
                            start = Time.realtimeSinceStartup;
                        }
                    }

                    if (vertsToRemove.Count == 0)
                    {
                        // We did not find any vertices to remove, so move to the next mesh.
                        continue;
                    }

                    // We found vertices to remove, so now we need to remove any triangles that reference these vertices.
                    int[] indices = mesh.GetTriangles(0);
                    List<int> updatedIndices = new List<int>();

                    for (int index = 0; index < indices.Length; index += 3)
                    {
                        // Each triangle utilizes three slots in the index buffer, check to see if any of the
                        // triangle indices contain a vertex that should be removed.
                        if (vertsToRemove.Contains(indices[index]) ||
                            vertsToRemove.Contains(indices[index + 1]) ||
                            vertsToRemove.Contains(indices[index + 2]))
                        {
                            // Do nothing, we don't want to save this triangle...
                        }
                        else
                        {
                            // Every vertex in this triangle is good, so let's save it.
                            updatedIndices.Add(indices[index]);
                            updatedIndices.Add(indices[index + 1]);
                            updatedIndices.Add(indices[index + 2]);
                        }

                        // If too much time has passed, we need to return control to the main game loop.
                        if ((Time.realtimeSinceStartup - start) > FrameTime)
                        {
                            // Pause our work, and continue making additional planes on the next frame.
                            yield return null;
                            start = Time.realtimeSinceStartup;
                        }
                    }

                    if (indices.Length == updatedIndices.Count)
                    {
                        // None of the verts to remove were being referenced in the triangle list.
                        continue;
                    }

                    // Update mesh to use the new triangles.
                    mesh.SetTriangles(updatedIndices.ToArray(), 0);
                    mesh.RecalculateBounds();
                    yield return null;
                    start = Time.realtimeSinceStartup;

                    // Reset the mesh collider to fit the new mesh.
                    MeshCollider meshCollider = filter.gameObject.GetComponent<MeshCollider>();
                    if (meshCollider != null)
                    {
                        meshCollider.sharedMesh = null;
                        meshCollider.sharedMesh = mesh;
                    }
                }
            }

            Debug.Log("Finished removing vertices.");

            // We are done removing vertices, trigger an event.
            EventHandler handler = RemoveVerticesComplete;
            if (handler != null)
            {
                handler(this, EventArgs.Empty);
            }

            removingVerts = false;
        }
    }
}