using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Threading;
using FastArena.Network.Packets;
using UnityEngine;
using Object = UnityEngine.Object;

namespace FastArena.Network
{
    public enum ClientStatus
    {
        Initialized, Connected, Disconnected
    }

    public class ServerConnection
    {
        public IPAddress Ip;
        public short PortFirst;
        public Guid SessionId;
        public byte[] ServerKey;
    }
    
    public static class Client
    {
        private static TcpClient _tcp;
        private static UdpClient _udp;
        private static byte[] _clientKey;
        private static ServerConnection _serverConnection;
        
        public static ClientStatus Status { get; private set; } = ClientStatus.Initialized;
        
        public static void SendUserAction()
        {
            throw new NotImplementedException();
        }

        public static void Connect(IPAddress ip, short portFirst)
        {
            _tcp = new TcpClient();
            _udp = new UdpClient();
            _serverConnection = new ServerConnection()
            {
                Ip = ip, PortFirst = portFirst
            };
            _udp.Connect(ip, portFirst + 1);
            _tcp.BeginConnect(ip, portFirst, TCPConnectCallback, null);
        }

        private static void ReadExactly(this TcpClient tcp, byte[] buffer, int offset, int exactSize, int timeoutMs = 5000)
        {
            int msPassed = 0;
            while (tcp.Available < exactSize)
            {
                Thread.Sleep(1);
                msPassed += 1;
                if (msPassed > timeoutMs)
                {
                    throw new TimeoutException(
                        $"couldn't get {exactSize} bytes from socket, {tcp.Available} avaliable");
                }
            }

            int readN = _tcp.GetStream().Read(buffer, offset, exactSize);
            if (readN != exactSize)
                throw new Exception($"expected to read {exactSize} bytes, got {readN}");
        }
        
        private static void TCPConnectCallback(IAsyncResult ar)
        {
            _tcp.EndConnect(ar);
            
            // receive first response from server
            byte[] buffer = new byte[4096];
            _tcp.ReadExactly(buffer, 0, Marshal.SizeOf<ConnectionResponseTCP>());
            //TODO: implement decryption
            var firstResponse = StructBinaryConverter.ReadStruct<ConnectionResponseTCP>(buffer);
            firstResponse.ValidateResponseCode();
            _serverConnection.SessionId = firstResponse.sessionId;
            _serverConnection.ServerKey = firstResponse.serverKey;
            
            // connect udp socket associated with current session
            byte[] byteMessage = StructBinaryConverter.GetBytes(new ConnectionRequestUDP(_serverConnection.SessionId));
            //TODO: implement encryption
            _udp.Send(byteMessage, byteMessage.Length);
            
            // receive second response from server
            IPEndPoint remoteEndpoint = new IPEndPoint(IPAddress.None, 0);
            byteMessage = _udp.Receive(ref remoteEndpoint);
            //TODO: implement decryption
            var secondResponse = StructBinaryConverter.ReadStruct<ConnectionResponseUDP>(byteMessage);
            secondResponse.ValidateResponseCode();
            
            //TODO: start tcp read loop
            //TODO: start udp read loop
        }

        
        private static readonly Dictionary<int, SyncTransform> _syncTransforms = new Dictionary<int, SyncTransform>();

        private static void CreateGameObject(ref CreateObjectPacket packet)
        {
            var prefab = Resources.Load(packet.prefabPath, typeof(GameObject));
            var go = (GameObject)Object.Instantiate(prefab);
            var idComponent = go.AddComponent<GameObjectId>();
            idComponent._SetId(packet.gameObjectId);
            if (go.TryGetComponent(out SyncTransform syncTransform))
            {
                _syncTransforms.Add(packet.gameObjectId, syncTransform);
            }
        }
        
        private static void UpdateObjectTransform(ref TransformUpdatePacket packet)
        {
            if(!_syncTransforms.TryGetValue(packet.gameObjectId, out var component))
                return;
            component.UpdateTransform(ref packet);
        }
    }
}