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

using System;
using UnityEngine;
using HoloToolkit.Unity.SpatialMapping;

namespace HoloToolkit.Unity
{
    /// <summary>
    /// The SpatialUnderstanding class controls the state and flow of the 
    /// scanning process used in the understanding module. 
    /// </summary>
    [RequireComponent(typeof(SpatialUnderstandingSourceMesh))]
    [RequireComponent(typeof(SpatialUnderstandingCustomMesh))]
    public class SpatialUnderstanding : Singleton<SpatialUnderstanding>
    {
        // Consts
        public const float ScanSearchDistance = 8.0f;

        // Enums
        public enum ScanStates
        {
            None,
            ReadyToScan,
            Scanning,
            Finishing,
            Done
        }

        // Config
        [Tooltip("If set to false, scanning will only begin after RequestBeginScanning is called")]
        public bool AutoBeginScanning = true;
        [Tooltip("Update period used during the scanning process (typically faster than after scanning is completed)")]
        public float UpdatePeriod_DuringScanning = 1.0f;
        [Tooltip("Update period used after the scanning process is completed")]
        public float UpdatePeriod_AfterScanning = 4.0f;

        // Properties
        /// <summary>
        /// Switch used by the entire SpatialUnderstanding module to activate processing.
        /// </summary>
        public bool AllowSpatialUnderstanding
        {
            get
            {
                return true;
            }
        }

        /// <summary>
        /// Reference to the SpatialUnderstandingDLL class (wraps the understanding dll functions).
        /// </summary>
        public SpatialUnderstandingDll UnderstandingDLL { get; private set; }
        /// <summary>
        /// Reference to the SpatialUnderstandingSourceMesh behavior (input mesh data for the understanding module).
        /// </summary>
        public SpatialUnderstandingSourceMesh UnderstandingSourceMesh { get; private set; }
        /// <summary>
        /// Reference to the UnderstandingCustomMesh behavior (output mesh data from the understanding module).
        /// </summary>
        public SpatialUnderstandingCustomMesh UnderstandingCustomMesh { get; private set; }
        /// <summary>
        /// Indicates the current state of the scan process
        /// </summary>
        public ScanStates ScanState
        {
            get
            {
                return scanState;
            }
            private set
            {
                scanState = value;
                if (ScanStateChanged != null)
                {
                    ScanStateChanged();
                }

                // Update scan period, based on state
                SpatialMappingManager.Instance.GetComponent<SpatialMappingObserver>().TimeBetweenUpdates = (scanState == ScanStates.Done) ? UpdatePeriod_AfterScanning : UpdatePeriod_DuringScanning;
            }
        }
        /// <summary>
        /// Indicates the the scanning statistics are still being processed.
        /// Request finish should not be called when this is true. 
        /// </summary>
        public bool ScanStatsReportStillWorking
        {
            get
            {
                if (AllowSpatialUnderstanding)
                {
                    SpatialUnderstandingDll.Imports.PlayspaceStats stats = UnderstandingDLL.GetStaticPlayspaceStats();
                    return (stats.IsWorkingOnStats != 0);
                }
                return false;
            }
        }

        public delegate void OnScanDoneDelegate();

        // Events
        /// <summary>
        /// Event indicating that the scan is done
        /// </summary>
        public event OnScanDoneDelegate OnScanDone;

        /// <summary>
        /// Event indicating that the scan state has changed
        /// </summary>
        public event Action ScanStateChanged;

        // Privates
        private ScanStates scanState;

        private float timeSinceLastUpdate = 0.0f;

        // Functions
        protected override void Awake()
        {
            base.Awake();

            // Cache references to required component
            UnderstandingDLL = new SpatialUnderstandingDll();
            UnderstandingSourceMesh = GetComponent<SpatialUnderstandingSourceMesh>();
            UnderstandingCustomMesh = GetComponent<SpatialUnderstandingCustomMesh>();
        }

        private void Start()
        {
            // Initialize the DLL
            if (AllowSpatialUnderstanding)
            {
                SpatialUnderstandingDll.Imports.SpatialUnderstanding_Init();
            }
        }

        private void Update()
        {
            if (!AllowSpatialUnderstanding)
            {
                return;
            }

            // Only update every few frames, and only if we aren't pulling in a mesh 
            // already.
            timeSinceLastUpdate += Time.deltaTime;
            if ((!UnderstandingCustomMesh.IsImportActive) &&
                (Time.frameCount % 3 == 0))
            {
                // Real-Time scan
                Update_Scan(timeSinceLastUpdate);
                timeSinceLastUpdate = 0;
            }
        }

        protected override void OnDestroy()
        {
            // Term the DLL
            if (AllowSpatialUnderstanding)
            {
                SpatialUnderstandingDll.Imports.SpatialUnderstanding_Term();
            }

            base.OnDestroy();
        }

        /// <summary>
        /// Call to request that scanning should begin. If AutoBeginScanning
        /// is false, this function should be used to initiate the scanning process.
        /// </summary>
        public void RequestBeginScanning()
        {
            if (ScanState == ScanStates.None)
            {
                ScanState = ScanStates.ReadyToScan;
            }
        }

        /// <summary>
        /// Call to request that the scanning progress be finishing. The application must do
        /// this to finalize the playspace. The scan state will not progress pass
        /// Scanning until this is called. The spatial understanding queries will not function
        /// until the playspace is finalized.
        /// </summary>
        public void RequestFinishScan()
        {
            if (AllowSpatialUnderstanding)
            {
                SpatialUnderstandingDll.Imports.GeneratePlayspace_RequestFinish();
                ScanState = ScanStates.Finishing;
            }
        }

        /// <summary>
        /// Update the scan progress. This function will initialize the scan, update it, 
        /// and issue a final mesh import, when the scan is complete.
        /// </summary>
        /// <param name="deltaTime">The amount of time that has passed since the last update (typically Time.deltaTime)</param>
        private void Update_Scan(float deltaTime)
        {
            // If we auto-start scanning, do it now
            if (AutoBeginScanning &&
                (ScanState == ScanStates.None))
            {
                RequestBeginScanning();
            }

            // Update the scan
            bool scanDone = false;
            if (((ScanState == ScanStates.ReadyToScan) ||
                 (ScanState == ScanStates.Scanning) ||
                 (ScanState == ScanStates.Finishing)) &&
                (AllowSpatialUnderstanding))
            {
                // Camera
                Transform cameraTransform = CameraCache.Main.transform;
                Vector3 camPos = cameraTransform.position;
                Vector3 camFwd = cameraTransform.forward;
                Vector3 camUp = cameraTransform.up;

                // If not yet initialized, do that now
                if (ScanState == ScanStates.ReadyToScan)
                {
                    SpatialUnderstandingDll.Imports.GeneratePlayspace_InitScan(
                        camPos.x, camPos.y, camPos.z,
                        camFwd.x, camFwd.y, camFwd.z,
                        camUp.x, camUp.y, camUp.z,
                        ScanSearchDistance, ScanSearchDistance);
                    ScanState = ScanStates.Scanning;
                }

                // Update
                int meshCount;
                IntPtr meshList;
                if (UnderstandingSourceMesh.GetInputMeshList(out meshCount, out meshList))
                {
                    var stopWatch = System.Diagnostics.Stopwatch.StartNew();

                    scanDone = SpatialUnderstandingDll.Imports.GeneratePlayspace_UpdateScan(
                        meshCount, meshList,
                        camPos.x, camPos.y, camPos.z,
                        camFwd.x, camFwd.y, camFwd.z,
                        camUp.x, camUp.y, camUp.z,
                        deltaTime) == 1;

                    stopWatch.Stop();

                    if (stopWatch.Elapsed.TotalMilliseconds > (1000.0 / 30.0))
                    {
                        Debug.LogWarningFormat("SpatialUnderstandingDll.Imports.GeneratePlayspace_UpdateScan took {0,9:N2} ms", stopWatch.Elapsed.TotalMilliseconds);
                    }
                }
            }

            // If it's done, finish up
            if ((ScanState == ScanStates.Finishing) &&
                (scanDone) &&
                (!UnderstandingCustomMesh.IsImportActive) &&
                (UnderstandingCustomMesh != null))
            {
                // Final mesh import
                StartCoroutine(UnderstandingCustomMesh.Import_UnderstandingMesh());

                // Mark it
                ScanState = ScanStates.Done;
                if (OnScanDone != null) OnScanDone.Invoke();
            }
        }
    }
}
