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

using System;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Threading;

namespace HoloToolkit.Unity
{
    public static class XdeGuestLocator
    {
        [StructLayout(LayoutKind.Sequential, Pack = 4)]
        struct XdePeerHostIdentifier
        {
            public Guid GuestDiscoveryGUID;
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
            public byte[] GuestMACAddress;
            public int PeerDiscoveryPort;
        }

        [StructLayout(LayoutKind.Sequential, Pack = 4)]
        struct XdePeerGuestIdentifier
        {
            public Guid GuestDiscoveryGUID;
            public int GuestTcpPort;
            public int GuestSvcVersion;
        }

        public static bool IsSearching { get; private set; }
        public static bool HasData { get; private set; }
        public static IPAddress GuestIpAddress { get; private set; }

        static XdeGuestLocator()
        {
            HasData = false;
            IsSearching = false;
        }

        public static void FindGuestAddressAsync()
        {
            if (IsSearching)
                return;

            ThreadPool.QueueUserWorkItem((_) =>
            {
                IsSearching = true;
                HasData = false;
                GuestIpAddress = IPAddress.None;

                UnicastIPAddressInformation internalSwitchAddressInfo = null;
                try
                {
                    internalSwitchAddressInfo = GetInternalSwitchAddressInfo();
                }
                catch (Exception)
                {
                    UnityEngine.Debug.LogError("Failed to locate internal switch adapter");
                }

                if (internalSwitchAddressInfo != null)
                {
                    using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp))
                    {
                        try
                        {
                            // Bind to next available UDP port for a listen operation
                            socket.Blocking = true;
                            socket.ReceiveTimeout = (int)TimeSpan.FromSeconds(5).TotalMilliseconds;

                            socket.Bind(new IPEndPoint(internalSwitchAddressInfo.Address, 0));
                            var localPort = (socket.LocalEndPoint as IPEndPoint).Port;

                            // Send out a probe to 'devices' connected to the internal switch
                            // listening on port 3553 (Microsoft Device Emulator specific)
                            var broadcastAddress = GetBroadcastAddressForAddress(internalSwitchAddressInfo.Address, internalSwitchAddressInfo.IPv4Mask);
                            var broadcastTarget = new IPEndPoint(broadcastAddress, 3553);

                            //
                            // WORKAROUND: We don't have easy access to WMI to go querying
                            // for virtual machine information so we just cover finding
                            // the first 255 potential candidates xx 00 - xx FF.
                            //
                            // It sounds like a lot but we're talking super tiny
                            // payloads on an internal interface. It's very fast.
                            //
                            for (int i = 0; i <= 0xFF; i++)
                            {
                                var probe = GenerateProbe(localPort, i);
                                socket.SendTo(probe, broadcastTarget);
                            }

                            // Return the endpoint information for the first 'device' that replies
                            // (we don't necessarily care about the returned identifier info)
                            var responseBytes = new byte[Marshal.SizeOf(typeof(XdePeerGuestIdentifier))];

                            EndPoint guestEndpoint = new IPEndPoint(broadcastAddress, 0);

                            socket.ReceiveFrom(responseBytes, ref guestEndpoint);
                            GuestIpAddress = (guestEndpoint as IPEndPoint).Address;
                            HasData = true;
                        }
                        catch (SocketException)
                        {
                            // Do nothing, our probe went unanswered or failed
                        }
                    }
                }

                IsSearching = false;
            });
        }

        private static UnicastIPAddressInformation GetInternalSwitchAddressInfo()
        {
            var internalSwitch = GetInternalNetworkSwitchInterface();
            return internalSwitch.GetIPProperties().UnicastAddresses.Where(a => a.Address.AddressFamily == AddressFamily.InterNetwork).FirstOrDefault();
        }

        private static NetworkInterface GetInternalNetworkSwitchInterface()
        {
            return NetworkInterface.GetAllNetworkInterfaces().Where(i => i.Name.Contains("Windows Phone Emulator")).FirstOrDefault();
        }

        private static IPAddress GetBroadcastAddressForAddress(IPAddress address, IPAddress mask)
        {
            var addressInt = BitConverter.ToInt32(address.GetAddressBytes(), 0);
            var maskInt = BitConverter.ToInt32(mask.GetAddressBytes(), 0);
            return new IPAddress(BitConverter.GetBytes((addressInt | ~maskInt)));
        }

        private static byte[] GenerateProbe(int port, int machineIndex)
        {
            var identifier = new XdePeerHostIdentifier();
            identifier.PeerDiscoveryPort = port;
            identifier.GuestDiscoveryGUID = new Guid("{963ef858-2efe-4eb4-8d2d-fed5408e6441}");
            identifier.GuestMACAddress = new byte[] { 0x02, 0xDE, 0xDE, 0xDE, 0xDE, (byte)machineIndex };

            return GetStructureBytes(identifier);
        }

        private static byte[] GetStructureBytes(object obj)
        {
            var bytes = new byte[Marshal.SizeOf(obj)];

            var handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
            Marshal.StructureToPtr(obj, handle.AddrOfPinnedObject(), false);
            handle.Free();

            return bytes;
        }
    }
}
