﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using UnityEngine;
using UnityEngine.VR.WSA;
using UnityEngine.VR.WSA.Sharing;
using Random = UnityEngine.Random;

public class DistributedHttpAnchorStore : MonoBehaviour
{
    public SignalRClient SignalRClient;
    public string AnchorEndpoint = "http://localhost:29281/api/anchor";

    private readonly Dictionary<string, GameObject> _gluedObjects = new Dictionary<string, GameObject>();
    private readonly HashSet<string> _knownAnchors = new HashSet<string>();

    // Use this for initialization
    void Start()
    {
        SignalRClient.On<string>("anchorHub", "anchorChanged", anchorName => StartCoroutine(DownloadAnchorIfExists(anchorName)));

#if UNITY_EDITOR
        Debug.LogWarning("In Unity editor - no world anchor support");
#endif
    }

    private IEnumerator DownloadAnchorIfExists(string anchorName)
    {
        var url = AnchorEndpoint + "/" + anchorName + "?cacheBust=" + Random.Range(1, 2000000000);
        Debug.Log("Requesting anchor " + anchorName + " at " + url);
        var request = new WWW(url);
        yield return request;

        if (request.error != null)
        {
            Debug.LogWarning("Didn't find anchor. Error was " + request.error);
            yield break; // Assume anchor doesn't exist
        }
        var bytes = request.bytes;

        Debug.Log(string.Format("Got {0} bytes for the anchor {1}", bytes.Length, anchorName));

        var isComplete = false;
        WorldAnchorTransferBatch.ImportAsync(bytes, (reason, batch) =>
        {
            Debug.Log("Imported " + anchorName + " to a batch: " + reason);
            BatchDownloaded(batch, ref isComplete);
        });
        yield return new WaitUntil(() => isComplete);
    }

    private void BatchDownloaded(WorldAnchorTransferBatch batch, ref bool isComplete)
    {
        foreach (var worldAnchorId in batch.GetAllIds())
        {
            Debug.Log(string.Format("Found world anchor for {0}", worldAnchorId));

            if (_gluedObjects.ContainsKey(worldAnchorId))
            {
                var targetGameObject = _gluedObjects[worldAnchorId];

                Debug.Log(string.Format("Attaching world anchor {0} to {1}", worldAnchorId, targetGameObject.name));
#if !UNITY_EDITOR
                var anchor = batch.LockObject(worldAnchorId, targetGameObject);
                anchor.name = worldAnchorId;
                Debug.Log("Anchor " + anchor.name + " isLocated=" + anchor.isLocated);
                anchor.OnTrackingChanged += delegate (WorldAnchor a, bool l) { Debug.Log("Anchor " + a.name + " isLocated=" + l); };
#endif
                Debug.Log(string.Format("Attached world anchor {0} to {1}", worldAnchorId, targetGameObject.name));
                Debug.Log(string.Format("Position: {0}", targetGameObject.transform.position));
            }
            _knownAnchors.Add(worldAnchorId);
        }
        isComplete = true;
    }

    public IEnumerator GlueTogether(string anchorName, GameObject targetGameObject)
    {
        _gluedObjects[anchorName] = targetGameObject;
#if UNITY_EDITOR
        yield break;
#else
        yield return DownloadAnchorIfExists(anchorName);

        if (!_knownAnchors.Contains(anchorName))
        {
            var anchor = GetOrCreateAnchor(anchorName, targetGameObject);
            yield return StartCoroutine(UploadAnchor(anchor));
    }
#endif
    }

    private static WorldAnchor GetOrCreateAnchor(string anchorName, GameObject targetGameObject)
    {
        var anchor = targetGameObject.GetComponents<WorldAnchor>().FirstOrDefault(a => a.name == anchorName);
        if (anchor == null)
        {
            Debug.Log("Creating anchor " + anchorName + " for " + targetGameObject.name + " at " + targetGameObject.transform.position);
            anchor = targetGameObject.AddComponent<WorldAnchor>();
            anchor.name = anchorName;
        }
        return anchor;
    }

    public IEnumerator SaveAnchor(string anchorName, GameObject targetGameObject)
    {
        if (!_gluedObjects.ContainsKey(anchorName) || _gluedObjects[anchorName] != targetGameObject)
        {
            Debug.LogError("anchor/game object mismatch: " + anchorName);
            yield break;
        }

#if !UNITY_EDITOR
        var anchor = GetOrCreateAnchor(anchorName, targetGameObject);
        yield return StartCoroutine(UploadAnchor(anchor));
#endif
    }

    public void DetachAnchor(string anchorName, GameObject targetGameObject)
    {
        Debug.Log("Attempting to detach anchor " + anchorName + " from " + targetGameObject.name);
#if UNITY_EDITOR
        WorldAnchor anchor = null;
#else
        var anchor = targetGameObject.GetComponents<WorldAnchor>().FirstOrDefault(a => a.name == anchorName);
#endif
        if (anchor != null)
        {
            DestroyImmediate(anchor);
            _knownAnchors.Remove(anchorName);
            Debug.Log("Detached anchor " + anchorName + " from " + targetGameObject.name);
        }
    }

    private IEnumerator UploadAnchor(WorldAnchor anchor)
    {
        Debug.Log("Uploading anchor " + anchor.name + ". Located? " + anchor.isLocated);
        bool isComplete = false;
        Action onComplete = () => isComplete = true;
        TrySerializeBatch(anchor, 5, onComplete);
        yield return new WaitUntil(() => isComplete);
        Debug.Log("Anchor upload complete.");
    }

    private void TrySerializeBatch(WorldAnchor anchor, int retries, Action onComplete)
    {
        Debug.Log("Serializing anchor " + anchor.name + " into batch.");
        var batch = new WorldAnchorTransferBatch();
        batch.AddWorldAnchor(anchor.name, anchor);

        var memoryStream = new MemoryStream();
        WorldAnchorTransferBatch.ExportAsync(batch,
            bytes => memoryStream.Write(bytes, 0, bytes.Length),
            reason =>
            {
                StartCoroutine(BatchSerialized(anchor, retries, reason, memoryStream, onComplete));
            }
        );
    }

    private IEnumerator BatchSerialized(WorldAnchor anchor, int retries, SerializationCompletionReason reason, MemoryStream memoryStream, Action onComplete)
    {
        Debug.Log("Serialized anchor " + anchor.name + " into batch. " + reason);

        if (reason != SerializationCompletionReason.Succeeded)
        {
            Debug.LogWarning("Deserialize failed. Why? Maybe: " + reason);
            if (retries > 1)
            {
                TrySerializeBatch(anchor, retries - 1, onComplete);
                yield break;
            }
        }
        else
        {
            var bytes = memoryStream.ToArray();

            Debug.Log("POST: " + Convert.ToBase64String(MD5.Create().ComputeHash(bytes)));

            var url = AnchorEndpoint + "/" + anchor.name;
            Debug.LogWarning("Posting anchor to " + url + " ( " + bytes.Length + " bytes)");
            var www = new WWW(url, bytes);
            yield return www;

            if (www.error == null)
            {
                Debug.LogWarning("Anchor " + anchor.name + " uploaded");
                _knownAnchors.Add(anchor.name);

                SignalRClient.SendToServer("anchorHub", "anchorChanged", new object[] { anchor.name });
            }
            else
            {
                Debug.LogWarning("Anchor upload failed: " + www.error);
            }
        }
        onComplete();
    }
}
