// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.MixedReality.Toolkit.SpatialAwareness;
using Microsoft.MixedReality.Toolkit.Utilities;
using Unity.Profiling;
using UnityEngine;

namespace Microsoft.MixedReality.Toolkit.SpatialObjectMeshObserver
{
    /// <summary>
    /// Spatial awareness mesh observer that provides mesh data from a 3D model imported as a Unity asset.
    /// </summary>
    [MixedRealityDataProvider(
        typeof(IMixedRealitySpatialAwarenessSystem),
        SupportedPlatforms.WindowsEditor | SupportedPlatforms.MacEditor | SupportedPlatforms.LinuxEditor,
        "Spatial Object Mesh Observer",
        "Providers/ObjectMeshObserver/Profiles/DefaultObjectMeshObserverProfile.asset",
        "MixedRealityToolkit.Core")]
    [HelpURL("https://docs.microsoft.com/windows/mixed-reality/mrtk-unity/features/spatial-awareness/spatial-awareness-getting-started")]
    public class SpatialObjectMeshObserver :
        BaseSpatialMeshObserver,
        IMixedRealityCapabilityCheck
    {
        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="registrar">The <see cref="IMixedRealityServiceRegistrar"/> instance that loaded the service.</param>
        /// <param name="name">Friendly name of the service.</param>
        /// <param name="priority">Service priority. Used to determine order of instantiation.</param>
        /// <param name="profile">The service's configuration profile.</param>
        [System.Obsolete("This constructor is obsolete (registrar parameter is no longer required) and will be removed in a future version of the Microsoft Mixed Reality Toolkit.")]
        public SpatialObjectMeshObserver(
            IMixedRealityServiceRegistrar registrar,
            IMixedRealitySpatialAwarenessSystem spatialAwarenessSystem,
            string name = null,
            uint priority = DefaultPriority,
            BaseMixedRealityProfile profile = null) : this(spatialAwarenessSystem, name, priority, profile)
        {
            Registrar = registrar;
        }

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="name">Friendly name of the service.</param>
        /// <param name="priority">Service priority. Used to determine order of instantiation.</param>
        /// <param name="profile">The service's configuration profile.</param>
        public SpatialObjectMeshObserver(
            IMixedRealitySpatialAwarenessSystem spatialAwarenessSystem,
            string name = null,
            uint priority = DefaultPriority,
            BaseMixedRealityProfile profile = null) : base(spatialAwarenessSystem, name, priority, profile)
        { }

        private bool sendObservations = true;

        private GameObject spatialMeshObject = null;

        #region BaseSpatialMeshObserver Implementation

        /// <summary>
        /// Reads the settings from the configuration profile.
        /// </summary>
        protected override void ReadProfile()
        {
            base.ReadProfile();

            SpatialObjectMeshObserverProfile profile = ConfigurationProfile as SpatialObjectMeshObserverProfile;
            if (profile == null) { return; }

            // SpatialObjectMeshObserver settings
            spatialMeshObject = profile.SpatialMeshObject;
        }

        #endregion BaseSpatialMeshObserver Implementation

        #region IMixedRealityCapabilityCheck Implementation

        /// <inheritdoc />
        bool IMixedRealityCapabilityCheck.CheckCapability(MixedRealityCapability capability)
        {
            return capability == MixedRealityCapability.SpatialAwarenessMesh;
        }

        #endregion IMixedRealityCapabilityCheck Implementation

        #region IMixedRealityDataProvider Implementation

        /// <inheritdoc />
        public override void Update()
        {
            if (!IsRunning)
            {
                return;
            }

            SendMeshObjects();
        }

        #endregion IMixedRealityDataProvider Implementation

        #region BaseSpatialObserver Implementation

        /// <inheritdoc />
        protected override void CreateObserver()
        {
            if (StartupBehavior == AutoStartBehavior.AutoStart)
            {
                Resume();
            }
        }

        /// <inheritdoc />
        protected override void CleanupObserver()
        {
            if (IsRunning)
            {
                Suspend();
            }
        }

        #endregion BaseSpatialObserver Implementation

        #region IMixedRealitySpatialAwarenessObserver Implementation

        private static readonly ProfilerMarker ClearObservationsPerfMarker = new ProfilerMarker("[MRTK] SpatialObjectMeshObserver.ClearObservations");

        /// <inheritdoc />
        public override void ClearObservations()
        {
            using (ClearObservationsPerfMarker.Auto())
            {
                if (IsRunning)
                {
                    Debug.Log("Cannot clear observations while the observer is running. Suspending this observer.");
                    Suspend();
                }

                foreach (int id in Meshes.Keys)
                {
                    RemoveMeshObject(id);
                }

                // Resend file observations when resumed.
                sendObservations = true;
            }
        }

        /// <inheritdoc />
        public override void Resume()
        {
            if (IsRunning) { return; }
            IsRunning = true;
        }

        /// <inheritdoc />
        public override void Suspend()
        {
            if (!IsRunning) { return; }
            IsRunning = false;
        }

        #endregion IMixedRealitySpatialAwarenessObserver Implementation

        #region Helpers

        private int currentMeshId = 0;

        private static readonly ProfilerMarker SendMeshObjectsPerfMarker = new ProfilerMarker("[MRTK] SpatialObjectMeshObserver.SendMeshObjects");

        /// <summary>
        /// Sends the observations using the mesh data contained within the configured 3D model.
        /// </summary>
        private void SendMeshObjects()
        {
            if (!sendObservations) { return; }

            using (SendMeshObjectsPerfMarker.Auto())
            {
                if (spatialMeshObject != null)
                {
                    MeshFilter[] meshFilters = spatialMeshObject.GetComponentsInChildren<MeshFilter>();
                    for (int i = 0; i < meshFilters.Length; i++)
                    {
                        SpatialAwarenessMeshObject meshObject = SpatialAwarenessMeshObject.Create(
                            meshFilters[i].sharedMesh,
                            MeshPhysicsLayer,
                            $"Spatial Object Mesh {currentMeshId}",
                            currentMeshId,
                            ObservedObjectParent);

                        meshObject.GameObject.transform.localPosition = meshFilters[i].transform.position;
                        meshObject.GameObject.transform.localRotation = meshFilters[i].transform.rotation;

                        ApplyMeshMaterial(meshObject);

                        meshes.Add(currentMeshId, meshObject);

                        meshEventData.Initialize(this, currentMeshId, meshObject);
                        Service?.HandleEvent(meshEventData, OnMeshAdded);

                        currentMeshId++;
                    }
                }

                sendObservations = false;
            }
        }

        private static readonly ProfilerMarker RemoveMeshObjectPerfMarker = new ProfilerMarker("[MRTK] SpatialObjectMeshObserver.RemoveMeshObject");

        /// <summary>
        /// Removes an observation.
        /// </summary>
        private void RemoveMeshObject(int meshId)
        {
            using (RemoveMeshObjectPerfMarker.Auto())
            {
                if (meshes.TryGetValue(meshId, out SpatialAwarenessMeshObject meshObject))
                {
                    // Remove the mesh object from the collection.
                    meshes.Remove(meshId);
                    if (meshObject != null)
                    {
                        SpatialAwarenessMeshObject.Cleanup(meshObject);
                    }

                    // Send the mesh removed event
                    meshEventData.Initialize(this, meshId, null);
                    Service?.HandleEvent(meshEventData, OnMeshRemoved);
                }
            }
        }

        /// <summary>
        /// Applies the appropriate material, based on the current of the <see cref="SpatialAwarenessMeshDisplayOptions"/> property. 
        /// </summary>
        /// <param name="meshObject">The <see cref="SpatialAwarenessMeshObject"/> for which the material is to be applied.</param>
        private void ApplyMeshMaterial(SpatialAwarenessMeshObject meshObject)
        {
            if (meshObject?.Renderer == null) { return; }

            bool enable = (DisplayOption != SpatialAwarenessMeshDisplayOptions.None);

            if (enable)
            {
                meshObject.Renderer.sharedMaterial = (DisplayOption == SpatialAwarenessMeshDisplayOptions.Visible) ?
                    VisibleMaterial :
                    OcclusionMaterial;

                meshObject.Collider.material = PhysicsMaterial;
            }

            meshObject.Renderer.enabled = enable;
        }

        #endregion Helpers
    }
}
