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

using HoloToolkit.Unity;

#if UNITY_WSA
using System;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_2017_2_OR_NEWER
using UnityEngine.XR.WSA;
using UnityEngine.XR.WSA.Persistence;
using UnityEngine.XR.WSA.Sharing;
#else
using UnityEngine.VR.WSA;
using UnityEngine.VR.WSA.Persistence;
using UnityEngine.VR.WSA.Sharing;
#endif
#endif


namespace HoloToolkit.Sharing
{
    /// <summary>
    /// Wrapper around world anchor store to streamline some of the persistence api busy work.
    /// </summary>
    public class SharingWorldAnchorManager : WorldAnchorManager
    {
#if UNITY_WSA
        /// <summary>
        /// Called once the anchor has fully uploaded.
        /// </summary>
        public event Action<bool> AnchorUploaded;

        /// <summary>
        /// Called when the anchor has been downloaded.
        /// </summary>
        public event Action<bool, GameObject> AnchorDownloaded;

        /// <summary>
        /// Sometimes we'll see a really small anchor blob get generated.
        /// These tend to not work, so we have a minimum trustworthy size.
        /// </summary>
        private const uint MinTrustworthySerializedAnchorDataSize = 100000;

        /// <summary>
        /// WorldAnchorTransferBatch is the primary object in serializing/deserializing anchors.
        /// <remarks>Only available on device.</remarks>
        /// </summary>
        private WorldAnchorTransferBatch currentAnchorTransferBatch;

        private bool isExportingAnchors;
        private bool shouldExportAnchors;

        /// <summary>
        /// The data blob of the anchor.
        /// </summary>
        private List<byte> rawAnchorUploadData = new List<byte>(0);

        private bool canUpdate;
        private bool isImportingAnchors;
        private bool shouldImportAnchors;

        /// <summary>
        /// The current download anchor data blob.
        /// </summary>
        private byte[] rawAnchorDownloadData;

        #region Unity Methods

        protected override void Start()
        {
            base.Start();

            if (SharingStage.Instance != null)
            {
                ShowDetailedLogs = SharingStage.Instance.ShowDetailedLogs;

                // SharingStage should be valid at this point, but we may not be connected.
                if (SharingStage.Instance.IsConnected)
                {
                    Connected();
                }
                else
                {
                    Disconnected();
                }
            }
            else
            {
                Debug.LogError("SharingWorldAnchorManager requires the SharingStage.");
            }
        }

        protected override void Update()
        {
            if (AnchorStore == null ||
                SharingStage.Instance == null ||
                SharingStage.Instance.CurrentRoom == null ||
                !canUpdate)
            { return; }

            if (LocalAnchorOperations.Count > 0)
            {
                if (!isExportingAnchors && !isImportingAnchors)
                {
                    DoAnchorOperation(LocalAnchorOperations.Dequeue());
                }
            }
            else
            {
                if (shouldImportAnchors && !isImportingAnchors && !isExportingAnchors)
                {
                    if (AnchorDebugText != null)
                    {
                        AnchorDebugText.text += "\nStarting Anchor Download...";
                    }

                    isImportingAnchors = true;
                    shouldImportAnchors = false;
                    WorldAnchorTransferBatch.ImportAsync(rawAnchorDownloadData, ImportComplete);
                }

                if (shouldExportAnchors && !isExportingAnchors && !isImportingAnchors)
                {
                    if (AnchorDebugText != null)
                    {
                        AnchorDebugText.text += "\nStarting Anchor Upload...";
                    }

                    isExportingAnchors = true;
                    shouldExportAnchors = false;
                    WorldAnchorTransferBatch.ExportAsync(currentAnchorTransferBatch, WriteBuffer, ExportComplete);
                }
            }
        }

        protected override void OnDestroy()
        {
            Disconnected();
            base.OnDestroy();
        }

        #endregion // Unity Methods

        #region Event Callbacks

        /// <summary>
        /// Callback function that contains the WorldAnchorStore object.
        /// </summary>
        /// <param name="anchorStore">The WorldAnchorStore to cache.</param>
        protected override void AnchorStoreReady(WorldAnchorStore anchorStore)
        {
            AnchorStore = anchorStore;

            if (!SharingStage.Instance.KeepRoomAlive || !PersistentAnchors)
            {
                if (AnchorDebugText != null)
                {
                    AnchorDebugText.text += "\nClearing Anchor Store...";
                }

                anchorStore.Clear();
            }
        }

        /// <summary>
        /// Called when the sharing stage connects to a server.
        /// </summary>
        /// <param name="sender">Sender.</param>
        /// <param name="e">Events Arguments.</param>
        private void Connected(object sender = null, EventArgs e = null)
        {
            SharingStage.Instance.SharingManagerConnected -= Connected;
            SharingStage.Instance.SharingManagerDisconnected += Disconnected;

            SharingStage.Instance.RoomManagerAdapter.UserJoinedRoomEvent += RoomManagerListener_OnRoomJoined;
            SharingStage.Instance.RoomManagerAdapter.UserLeftRoomEvent += RoomManagerListener_OnLeftRoom;

            if (ShowDetailedLogs)
            {
                Debug.Log("[SharingWorldAnchorManager] Connected to server.");
            }

            if (AnchorDebugText != null)
            {
                AnchorDebugText.text += "\nConnected to Server.";
            }
        }

        /// <summary>
        /// Called when the sharing stage is disconnected from a server.
        /// </summary>
        /// <param name="sender">Sender.</param>
        /// <param name="e">Event Arguments.</param>
        private void Disconnected(object sender = null, EventArgs e = null)
        {
            if (SharingStage.Instance != null)
            {
                SharingStage.Instance.SharingManagerConnected += Connected;
                SharingStage.Instance.SharingManagerDisconnected -= Disconnected;

                SharingStage.Instance.RoomManagerAdapter.UserJoinedRoomEvent -= RoomManagerListener_OnRoomJoined;
                SharingStage.Instance.RoomManagerAdapter.UserLeftRoomEvent -= RoomManagerListener_OnLeftRoom;
            }

            if (ShowDetailedLogs)
            {
                Debug.Log("[SharingWorldAnchorManager] Disconnected from server.");
            }

            if (AnchorDebugText != null)
            {
                AnchorDebugText.text += "\nDisconnected from Server.";
            }
            canUpdate = false;
        }

        private void RoomManagerListener_OnRoomJoined(Room room, int userId)
        {
            if (SharingStage.Instance.Manager.GetLocalUser().GetID() == userId &&
                SharingStage.Instance.CurrentRoom.GetID() == room.GetID())
            {
                SharingStage.Instance.RoomManagerAdapter.AnchorUploadedEvent += RoomManagerListener_AnchorUploaded;
                SharingStage.Instance.RoomManagerAdapter.AnchorsChangedEvent += RoomManagerListener_AnchorsChanged;
                SharingStage.Instance.RoomManagerAdapter.AnchorsDownloadedEvent += RoomManagerListener_AnchorDownloaded;

                canUpdate = true;

                if (ShowDetailedLogs)
                {
                    Debug.LogFormat("[SharingWorldAnchorManager] In room {0} with {1} anchors.",
                        SharingStage.Instance.CurrentRoom.GetName().GetString(),
                        SharingStage.Instance.CurrentRoom.GetAnchorCount());
                }

                if (AnchorDebugText != null)
                {
                    AnchorDebugText.text += string.Format("\nIn room {0} with {1} anchors.",
                        SharingStage.Instance.CurrentRoom.GetName().GetString(),
                        SharingStage.Instance.CurrentRoom.GetAnchorCount());
                }
            }
        }

        private void RoomManagerListener_OnLeftRoom(Room room, int userId)
        {
            if (SharingStage.Instance.Manager.GetLocalUser().GetID() == userId)
            {
                SharingStage.Instance.RoomManagerAdapter.AnchorUploadedEvent -= RoomManagerListener_AnchorUploaded;
                SharingStage.Instance.RoomManagerAdapter.AnchorsChangedEvent -= RoomManagerListener_AnchorsChanged;
                SharingStage.Instance.RoomManagerAdapter.AnchorsDownloadedEvent -= RoomManagerListener_AnchorDownloaded;

                canUpdate = false;

                if (ShowDetailedLogs)
                {
                    Debug.LogFormat("\n[SharingWorldAnchorManager] Left room {0}", room.GetName().GetString());
                }
                if (AnchorDebugText != null)
                {
                    AnchorDebugText.text += string.Format("\nLeft room {0}", room.GetName().GetString());
                }
            }
        }

        /// <summary>
        /// Called when anchor upload operations complete.
        /// </summary>
        private void RoomManagerListener_AnchorUploaded(bool successful, XString failureReason)
        {
            if (successful)
            {
                string[] anchorIds = currentAnchorTransferBatch.GetAllIds();

                for (int i = 0; i < anchorIds.Length; i++)
                {
                    if (ShowDetailedLogs)
                    {
                        Debug.LogFormat("[SharingWorldAnchorManager] Successfully uploaded anchor \"{0}\".", anchorIds[i]);
                    }

                    if (AnchorDebugText != null)
                    {
                        AnchorDebugText.text += string.Format("\nSuccessfully uploaded anchor \"{0}\".", anchorIds[i]);
                    }
                }
            }
            else
            {
                Debug.LogError("[SharingWorldAnchorManager] Upload failed: " + failureReason);
                if (AnchorDebugText != null)
                {
                    AnchorDebugText.text += string.Format("\nUpload failed: " + failureReason);
                }
            }

            rawAnchorUploadData.Clear();
            currentAnchorTransferBatch.Dispose();
            currentAnchorTransferBatch = null;
            isExportingAnchors = false;

            if (AnchorUploaded != null)
            {
                AnchorUploaded(successful);
            }
        }

        /// <summary>
        /// Called when anchor download operations complete.
        /// </summary>
        private void RoomManagerListener_AnchorDownloaded(bool successful, AnchorDownloadRequest request, XString failureReason)
        {
            // If we downloaded anchor data successfully we should import the data.
            if (successful)
            {
                int dataSize = request.GetDataSize();

                if (ShowDetailedLogs)
                {
                    Debug.LogFormat("[SharingWorldAnchorManager] Downloaded {0} bytes.", dataSize.ToString());
                }

                if (AnchorDebugText != null)
                {
                    AnchorDebugText.text += string.Format("\nDownloaded {0} bytes.", dataSize.ToString());
                }

                rawAnchorDownloadData = new byte[dataSize];
                request.GetData(rawAnchorDownloadData, dataSize);
                shouldImportAnchors = true;
            }
            else
            {
                Debug.LogWarning("[SharingWorldAnchorManager] Anchor DL failed " + failureReason);
                if (AnchorDebugText != null)
                {
                    AnchorDebugText.text += string.Format("\nAnchor DL failed " + failureReason);
                }
            }
        }

        /// <summary>
        /// Called when anchors are changed in the room.
        /// </summary>
        /// <param name="room">The room where the anchors were changed.</param>
        private void RoomManagerListener_AnchorsChanged(Room room)
        {
            if (SharingStage.Instance.CurrentRoom.GetID() == room.GetID())
            {
                if (AnchorDebugText != null)
                {
                    AnchorDebugText.text += "\nRoom Anchors Updated!  Clearing the local Anchor Store and attempting to download the update...";
                }

                // Clear our local anchor store, and download all our shared anchors again.
                // TODO: Only download the anchors that changed. Currently there's no way to know which anchor changed.
                AnchorStore.Clear();

                if (ShowDetailedLogs)
                {
                    Debug.LogFormat("[SharingWorldAnchorManager] Anchors updated for room \"{0}\".\nClearing the local Anchor Store and attempting to download the update...", room.GetName().GetString());
                }

                if (AnchorDebugText != null)
                {
                    AnchorDebugText.text += string.Format("\nAnchors updated for room \"{0}\".\nClearing the local Anchor Store and attempting to download the update...", room.GetName().GetString());
                }

                int roomAnchorCount = SharingStage.Instance.CurrentRoom.GetAnchorCount();

                for (int i = 0; i < roomAnchorCount; i++)
                {
                    GameObject anchoredObject;
                    string roomAnchorId = SharingStage.Instance.CurrentRoom.GetAnchorName(i).GetString();

                    if (AnchorGameObjectReferenceList.TryGetValue(roomAnchorId, out anchoredObject))
                    {
                        if (ShowDetailedLogs)
                        {
                            Debug.LogFormat("[SharingWorldAnchorManager] Found cached GameObject reference for \"{0}\".", roomAnchorId);
                        }

                        if (AnchorDebugText != null)
                        {
                            AnchorDebugText.text += string.Format("\nFound cached GameObject reference for \"{0}\".", roomAnchorId);
                        }

                        AttachAnchor(anchoredObject, roomAnchorId);
                    }
                    else
                    {
                        anchoredObject = GameObject.Find(roomAnchorId);

                        if (anchoredObject != null)
                        {
                            if (ShowDetailedLogs)
                            {
                                Debug.LogFormat("[SharingWorldAnchorManager] Found a GameObject reference form scene for \"{0}\".", roomAnchorId);
                            }

                            if (AnchorDebugText != null)
                            {
                                AnchorDebugText.text += string.Format("\nFound a GameObject reference form scene for \"{0}\".", roomAnchorId);
                            }

                            AttachAnchor(anchoredObject, roomAnchorId);
                        }
                        else
                        {
                            Debug.LogWarning("[SharingWorldAnchorManager] Unable to find a matching GameObject for anchor!");
                            if (AnchorDebugText != null)
                            {
                                AnchorDebugText.text += "\nUnable to find a matching GameObject for anchor!";
                            }
                        }
                    }
                }
            }
        }

        #endregion // Event Callbacks

        /// <summary>
        /// Called before creating anchor.  Used to check if import required.
        /// </summary>
        /// <param name="anchorId">Name of the anchor to import.</param>
        /// <param name="objectToAnchor">GameObject </param>
        /// <returns>Success.</returns>
        protected override bool ImportAnchor(string anchorId, GameObject objectToAnchor)
        {
            if (SharingStage.Instance == null ||
                SharingStage.Instance.Manager == null ||
                SharingStage.Instance.CurrentRoom == null)
            {
                Debug.LogErrorFormat("[SharingWorldAnchorManager] Failed to import anchor \"{0}\"!  The sharing service was not ready.", anchorId);
                if (AnchorDebugText != null)
                {
                    AnchorDebugText.text += string.Format("\nFailed to import anchor \"{0}\"!  The sharing service was not ready.", anchorId);
                }

                return false;
            }

            int roomAnchorCount = SharingStage.Instance.CurrentRoom.GetAnchorCount();

            for (int i = 0; i < roomAnchorCount; i++)
            {
                XString roomAnchorId = SharingStage.Instance.CurrentRoom.GetAnchorName(i);

                if (roomAnchorId.GetString().Equals(anchorId))
                {
                    bool downloadStarted = SharingStage.Instance.CurrentRoomManager.DownloadAnchor(SharingStage.Instance.CurrentRoom, anchorId);

                    if (downloadStarted)
                    {
                        if (ShowDetailedLogs)
                        {
                            Debug.Log("[SharingWorldAnchorManager] Found a match! Attempting to download anchor...");
                        }

                        if (AnchorDebugText != null)
                        {
                            AnchorDebugText.text += "\nFound a match! Attempting to download anchor...";
                        }
                    }
                    else
                    {
                        Debug.LogWarning("[SharingWorldAnchorManager] Found a match, but we've failed to start download!");
                        if (AnchorDebugText != null)
                        {
                            AnchorDebugText.text += "\nFound a match, but we've failed to start download!";
                        }
                    }

                    return downloadStarted;
                }
            }

            if (ShowDetailedLogs)
            {
                Debug.LogFormat("[SharingWorldAnchorManager] No matching anchor found for \"{0}\" in room {1}.", anchorId, SharingStage.Instance.CurrentRoom.GetName().GetString());
            }

            if (AnchorDebugText != null)
            {
                AnchorDebugText.text += string.Format("\nNo matching anchor found for \"{0}\" in room {1}.", anchorId, SharingStage.Instance.CurrentRoom.GetName().GetString());
            }

            return false;
        }

        /// <summary>
        /// Exports and uploads the anchor to the sharing service.
        /// </summary>
        /// <param name="anchor">The anchor to export.</param>
        /// <returns>Success</returns>
        protected override void ExportAnchor(WorldAnchor anchor)
        {
            if (SharingStage.Instance == null ||
                SharingStage.Instance.Manager == null ||
                SharingStage.Instance.CurrentRoom == null)
            {
                Debug.LogErrorFormat("[SharingWorldAnchorManager] Failed to export anchor \"{0}\"!  The sharing service was not ready.", anchor.name);
                if (AnchorDebugText != null)
                {
                    AnchorDebugText.text += string.Format("\nFailed to export anchor \"{0}\"!  The sharing service was not ready.", anchor.name);
                }

                return;
            }

            if (!shouldExportAnchors)
            {
                if (ShowDetailedLogs)
                {
                    Debug.LogWarningFormat("[SharingWorldAnchorManager] Attempting to export anchor \"{0}\".", anchor.name);
                }

                if (AnchorDebugText != null)
                {
                    AnchorDebugText.text += string.Format("\nAttempting to export anchor \"{0}\".", anchor.name);
                }

                if (currentAnchorTransferBatch == null)
                {
                    currentAnchorTransferBatch = new WorldAnchorTransferBatch();
                    if (AnchorDebugText != null)
                    {
                        AnchorDebugText.text += "\nCreating a new World Anchor Transfer Batch...";
                    }
                }
                else
                {
                    Debug.LogWarning("[SharingWorldAnchorManager] We didn't properly cleanup our WorldAnchorTransferBatch!");
                    if (AnchorDebugText != null)
                    {
                        AnchorDebugText.text += "\nWe didn't properly cleanup our WorldAnchorTransferBatch!";
                    }
                }

                currentAnchorTransferBatch.AddWorldAnchor(anchor.name, anchor);
                shouldExportAnchors = true;
            }
        }

        /// <summary>
        /// Called by the WorldAnchorTransferBatch when anchor exporting is complete.
        /// </summary>
        /// <param name="status">Serialization Status.</param>
        private void ExportComplete(SerializationCompletionReason status)
        {
            if (status == SerializationCompletionReason.Succeeded &&
                rawAnchorUploadData.Count > MinTrustworthySerializedAnchorDataSize)
            {
                if (ShowDetailedLogs)
                {
                    Debug.LogFormat("[SharingWorldAnchorManager] Exporting {0} anchors with {1} bytes.", currentAnchorTransferBatch.anchorCount.ToString(), rawAnchorUploadData.ToArray().Length.ToString());
                }

                if (AnchorDebugText != null)
                {
                    AnchorDebugText.text += string.Format("\nExporting {0} anchors with {1} bytes.",
                        currentAnchorTransferBatch.anchorCount.ToString(),
                        rawAnchorUploadData.ToArray().Length.ToString());
                }

                string[] anchorNames = currentAnchorTransferBatch.GetAllIds();

                for (var i = 0; i < anchorNames.Length; i++)
                {
                    SharingStage.Instance.Manager.GetRoomManager().UploadAnchor(
                        SharingStage.Instance.CurrentRoom,
                        new XString(anchorNames[i]),
                        rawAnchorUploadData.ToArray(),
                        rawAnchorUploadData.Count);
                }
            }
            else
            {
                Debug.LogWarning("[SharingWorldAnchorManager] Failed to upload anchor!");

                if (AnchorDebugText != null)
                {
                    AnchorDebugText.text += "\nFailed to upload anchor!";
                }

                if (rawAnchorUploadData.Count < MinTrustworthySerializedAnchorDataSize)
                {
                    Debug.LogWarning("[SharingWorldAnchorManager] Anchor data was not valid.  Try creating the anchor again.");

                    if (AnchorDebugText != null)
                    {
                        AnchorDebugText.text += "\nAnchor data was not valid.  Try creating the anchor again.";
                    }
                }
            }
        }

        /// <summary>
        /// Called when a remote anchor has been deserialized.
        /// </summary>
        /// <param name="status"></param>
        /// <param name="anchorBatch"></param>
        private void ImportComplete(SerializationCompletionReason status, WorldAnchorTransferBatch anchorBatch)
        {
            bool successful = status == SerializationCompletionReason.Succeeded;
            GameObject objectToAnchor = null;

            if (successful)
            {
                if (ShowDetailedLogs)
                {
                    Debug.LogFormat("[SharingWorldAnchorManager] Successfully imported \"{0}\" anchors.", anchorBatch.anchorCount.ToString());
                }

                if (AnchorDebugText != null)
                {
                    AnchorDebugText.text += string.Format("\nSuccessfully imported \"{0}\" anchors.", anchorBatch.anchorCount.ToString());
                }

                string[] anchorNames = anchorBatch.GetAllIds();

                for (var i = 0; i < anchorNames.Length; i++)
                {
                    if (AnchorGameObjectReferenceList.TryGetValue(anchorNames[i], out objectToAnchor))
                    {
                        AnchorStore.Save(anchorNames[i], anchorBatch.LockObject(anchorNames[i], objectToAnchor));
                    }
                    else
                    {
                        //TODO: Figure out how to get the GameObject reference from across the network.  For now it's best to use unique GameObject names.
                        Debug.LogWarning("[SharingWorldAnchorManager] Unable to import anchor!  We don't know which GameObject to anchor!");

                        if (AnchorDebugText != null)
                        {
                            AnchorDebugText.text += "\nUnable to import anchor!  We don\'t know which GameObject to anchor!";
                        }
                    }
                }
            }
            else
            {
                Debug.LogError("[SharingWorldAnchorManager] Import failed!");

                if (AnchorDebugText != null)
                {
                    AnchorDebugText.text += "\nImport failed!";
                }
            }

            if (AnchorDownloaded != null)
            {
                AnchorDownloaded(successful, objectToAnchor);
            }

            anchorBatch.Dispose();
            rawAnchorDownloadData = null;
            isImportingAnchors = false;
        }

        /// <summary>
        /// Called by the WorldAnchorTransferBatch as anchor data is available.
        /// </summary>
        /// <param name="data"></param>
        private void WriteBuffer(byte[] data)
        {
            rawAnchorUploadData.AddRange(data);
        }
#endif
    }
}
