// 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.Generic;
using System.IO;
#if UNITY_EDITOR || UNITY_STANDALONE_WIN
using System.Net;
using System.Net.Sockets;
#endif
using UnityEngine;

namespace HoloToolkit.Unity.SpatialMapping
{
    /// <summary>
    /// RemoteMeshTarget will listen for meshes being sent from a remote system (HoloLens).
    /// It is intended to run in the Unity Editor with exactly one
    /// HoloLens device sending data.
    /// </summary>
    public class RemoteMeshTarget : SpatialMappingSource
    {
        [Tooltip("The IPv4 Address of the machine running the Unity editor.")]
        public string ServerIP;

        [Tooltip("The connection port on the machine to use.")]
        public int ConnectionPort = 11000;

#if UNITY_EDITOR || UNITY_STANDALONE_WIN
        /// <summary>
        /// Listens for network connections over TCP.
        /// </summary> 
        private TcpListener networkListener;

        /// <summary>
        /// Keeps client information when a connection happens.
        /// </summary>
        private TcpClient networkClient;

        /// <summary>
        /// Tracks if a client is connected.
        /// </summary>
        private bool clientConnected;

        // Use this for initialization.
        private void Start()
        {
            // Setup the network listener.
            IPAddress localAddr = IPAddress.Parse(ServerIP.Trim());
            networkListener = new TcpListener(localAddr, ConnectionPort);
            networkListener.Start();

            // Request the network listener to wait for connections asynchronously.
            AsyncCallback callback = OnClientConnect;
            networkListener.BeginAcceptTcpClient(callback, this);
        }

        // Update is called once per frame.
        private void Update()
        {
            // If we have a connected client, presumably the client wants to send some meshes.
            if (clientConnected)
            {
                // Get the clients stream.
                NetworkStream stream = networkClient.GetStream();

                // Make sure there is data in the stream.
                if (stream.DataAvailable)
                {
                    // The first 4 bytes will be the size of the data containing the mesh(es).
                    int datasize = ReadInt(stream);

                    // Allocate a buffer to hold the data.  
                    byte[] dataBuffer = new byte[datasize];

                    // Read the data.
                    // The data can come in chunks. 
                    int readsize = 0;

                    while (readsize != datasize)
                    {
                        readsize += stream.Read(dataBuffer, readsize, datasize - readsize);
                    }

                    if (readsize != datasize)
                    {
                        Debug.Log("reading mesh failed: " + readsize + " != " + datasize);
                    }

                    // Pass the data to the mesh serializer. 
                    List<Mesh> meshes = new List<Mesh>(SimpleMeshSerializer.Deserialize(dataBuffer));

                    if (meshes.Count > 0)
                    {
                        // Use the network-based mapping source to receive meshes in the Unity editor.
                        SpatialMappingManager.Instance.SetSpatialMappingSource(this);
                    }

                    // For each mesh, create a GameObject to render it.
                    for (int index = 0; index < meshes.Count; index++)
                    {
                        int meshID = SurfaceObjects.Count;

                        SurfaceObject surface = CreateSurfaceObject(
                            mesh: meshes[index],
                            objectName: "Beamed-" + meshID,
                            parentObject: transform,
                            meshID: meshID
                            );

                        surface.Object.transform.parent = SpatialMappingManager.Instance.transform;

                        AddSurfaceObject(surface);
                    }

                    // Finally disconnect.
                    clientConnected = false;
                    networkClient.Close();

                    // And wait for the next connection.
                    AsyncCallback callback = OnClientConnect;
                    networkListener.BeginAcceptTcpClient(callback, this);
                }
            }
        }

        /// <summary>
        /// Reads an int from the next 4 bytes of the supplied stream.
        /// </summary>
        /// <param name="stream">The stream to read the bytes from.</param>
        /// <returns>An integer representing the bytes.</returns>
        private int ReadInt(Stream stream)
        {
            // The bytes arrive in the wrong order, so swap them.
            byte[] bytes = new byte[4];
            stream.Read(bytes, 0, 4);
            byte t = bytes[0];
            bytes[0] = bytes[3];
            bytes[3] = t;

            t = bytes[1];
            bytes[1] = bytes[2];
            bytes[2] = t;

            // Then bitconverter can read the int32.
            return BitConverter.ToInt32(bytes, 0);
        }

        /// <summary>
        /// Called when a client connects.
        /// </summary>
        /// <param name="result">The result of the connection.</param>
        private void OnClientConnect(IAsyncResult result)
        {
            if (result.IsCompleted)
            {
                networkClient = networkListener.EndAcceptTcpClient(result);
                if (networkClient != null)
                {
                    Debug.Log("Connected");
                    clientConnected = true;
                }
            }
        }
#endif
    }
}