using System.Linq;
using UnityEngine;
using UnityEngine.Networking;
using System.Collections;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Net;
using System.Text;
using System;

public class Client : MonoBehaviour
{
    public const int socketPort = 87;
    public const int webglPort = 86;
    public uint account_id = 0; //id акка
    string ACCOUNT_NAME = "";

    bool login_sent;
    bool entered = false;
    bool dc = false;    
    bool connected = false;    
    int lag = 0;
    double last_ping_time = 0;
    static string[] Servers = { "dev.prmsys.net", "localhost", "corp.prmsys.net" };
    static bool ShowPacketsSent = true;
    //private const bool ShowPacketsSent = false;

    // private const bool ShowPackets = true; //ставится только в редакторе
    static bool ShowPackets = false; //ставится только в редакторе
    double initial_timer;
    int internal_timer = 0;

    internal void SendGetUsers(object p)
    {
        throw new NotImplementedException();
    }

    public double timer
    {
        get
        {
            return DateTime.Now.Ticks / 10000000.0;    //секунды
        }
    }
    public static Client instance;
    public static string ServerName;
    public static bool StartConnectFlag = false;
#if UNITY_WEBGL
    public static WebSocket Mysocket;
#else
    public static Socket Mysocket;
#endif
    Dictionary<int, int> PacketLength = new Dictionary<int, int>();
    public delegate void OnReceive(byte[] bytedata);
    public static OnReceive[] packets = new OnReceive[255];
    private static Dictionary<string, UnityEngine.Object> loaded_models = new Dictionary<string, UnityEngine.Object>();
    private static Dictionary<string, UnityEngine.Object> loaded_icons = new Dictionary<string, UnityEngine.Object>();
    public string _dataPath = "";
    GameObject drone;
    public static ClientType clientType;
    public enum ClientType
    {
        Desktop,
        Web
    }

    public static void SendEnqueue(byte[] bytedata)
    {
        SendQueue.Enqueue(bytedata);
    }

    private void Awake()
    {
        instance = this;
        ServerName = PlayerPrefs.GetString("server");
        if (ServerName == "")
            ServerName = Servers[0];

        Register(1, Disconnect, 5);
        Register(2, Login, 5);
        Register(3, CoordinatesReceive);
        Register(30, Myping, 3);
        Register(47, UsersReceive);
        Register(48, BeaconsReceive);

        //set data path
        //if (Application.platform == RuntimePlatform.WindowsWebPlayer ||
        //    Application.platform == RuntimePlatform.OSXWebPlayer)
        if (Application.platform == RuntimePlatform.WebGLPlayer)
        {
            _dataPath = Application.dataPath + "/StreamingAssets";
            clientType = ClientType.Web;
        }
        else if (Application.platform == RuntimePlatform.Android)
        {
            _dataPath = "jar:file://" + Application.dataPath + "!/assets";
            clientType = ClientType.Web;
        }
        else
        {
            _dataPath = "file://" + Application.dataPath + "/StreamingAssets"; ;
            clientType = ClientType.Desktop;
        }
        _dataPath = Application.streamingAssetsPath;
        //Debug.Log("_dataPath " + _dataPath);
    }
    public int LagCount
    {
        get
        {
            return lagpoints.Count;
        }
    }

    int totallag
    {
        get
        {
            int q = 0;
            foreach (var g in lagpoints)
            {
                q += g;
            }
            return q;
        }
    }

    Queue<int> lagpoints = new Queue<int>();

    public int Averagelag
    {
        get
        {
            if (lagpoints.Count > 0)
                return totallag / lagpoints.Count;
            return 0;
        }
    }

    public void CoordinatesReceive(byte[] bytedata)
    {
        //var coordleng = bytedata.Length - 11;
        var accid = BitConverter.ToUInt32(bytedata, 5);
        var locid = BitConverter.ToUInt32(bytedata, 9);
        //WorkerController.TestStructures.Clear();
        //print("len " + BitConverter.ToUInt32(bytedata, 1));
        var worker = new List<Structure>();
        for (var read = 13; read < bytedata.Length; read += 20)
        {
            var id = BitConverter.ToUInt32(bytedata, read);
            var x = BitConverter.ToInt32(bytedata, read+4);
            var y = BitConverter.ToInt32(bytedata, read+8);
            var ticks = BitConverter.ToInt64(bytedata, read+12);
            //print($"coord accid {accid} locid {locid} {id} x {x} y{y} ticks {ticks}");

            //WorkerController.markers.ElementAt(1).marker.transform.position = new Vector3(x, 0.5f, y); 

            //WorkerController.TestStructures
            worker.Add(new Structure
            {
                id = id,
                coord_x = x,
                coord_y = y,
                ts = new DateTime(ticks),
                acc_id = accid,
                location_id = locid,
                zone_id = 1
            });
        }
        WorkerController.Workers[accid] = worker;
        WorkerController.end_send[accid] = true;
    }

    public void Myping(byte[] bytedata)
    {
        last_ping_time = timer;
        int lag = BitConverter.ToUInt16(bytedata, 1);
        //print("Myping " + lag);
        this.lag = lag;
        if (lagpoints.Count > 5)
            lagpoints.Dequeue();

        lagpoints.Enqueue(lag);

        List<byte> list = new List<byte>();
        list.Add(30);
        list.Add((byte)1);
        SendEnqueue(list.ToArray());
    }

    public void Disconnect(byte[] bytedata)
    {
        uint bid = BitConverter.ToUInt32(bytedata, 1);
        if (bid == account_id)
            Exit();
        else
        {
            print("disc id " + bid);
        }
    }


    public void Exit()
    {
        print("Client Exit");
        connected = false;
        login_sent = false;
        if (Mysocket != null)
        {
            Mysocket.Close();
            Mysocket = null;
        }
        connect_started = new DateTime();
        totalbytes = 0;
        account_id = 0;
        ACCOUNT_NAME = "";
        connected = false;
        sendDone = false;
        receiveDone = false;
        connectDone = false;
        StartConnectFlag = false;
        UnityEngine.SceneManagement.SceneManager.LoadScene(0);
    }

    //пакет с 1 параметром = 1
    public static void SendOneBytePacket(int num)
    {
        SendOneByteParamPacket(num, 1);
    }

    public static void SendOneByteTwoParamsPacket(int num, int param1, int param2)
    {
        byte[] data = { (Byte)num, (Byte)param1, (byte)param2 };
        SendEnqueue(data);
    }

    public static void SendThreeParamsIntPacket(int num, int param1, int param2, int param3)
    {
        List<byte> list = new List<byte>();
        list.Add((Byte)num);
        list.AddRange(BitConverter.GetBytes(param1));
        list.AddRange(BitConverter.GetBytes(param2));
        list.AddRange(BitConverter.GetBytes(param3));
        SendEnqueue(list.ToArray());
    }

    //пакет с 1 1-байтовым параметром
    public static void SendOneByteParamPacket(int num, int param)
    {
        byte[] data = { (Byte)num, (Byte)param };
        byteSend(data);
    }

    //пакет с 1 4-байтовым параметром
    public static void SendTwoByteParamPacket(int num, ushort param)
    {
        List<byte> list = new List<byte>();
        list.Add((Byte)num);
        list.AddRange(BitConverter.GetBytes(param));
        SendEnqueue(list.ToArray());
    }

    //пакет с 1 4-байтовым параметром
    public static void SendFourByteParamPacket(int num, uint param)
    {
        List<byte> list = new List<byte>();
        list.Add((Byte)num);
        list.AddRange(BitConverter.GetBytes(param));
        SendEnqueue(list.ToArray());
    }

    public static DateTime connect_started;
    public void Login(byte[] bytedata)
    {
        uint accid = BitConverter.ToUInt32(bytedata, 1);
        if (accid == 0xFFFFFFFF)
        {
            Debug.LogError("Login or password incorrect");
            AuthorizationController.error = true;
            return;
        }
        else if (accid == 0xFFFFFFFE)
        {
            Debug.LogError("Account was already connected");          
            return;
        }
        else if (accid == 0xFFFFFFFD)
        {
            Debug.LogError("Incorrect client version");           
            return;
        }
        AuthorizationController.success = true;
        account_id = accid;
        //var ts = DateTime.Now.Ticks-1000000000000;
        //var te = DateTime.Now.Ticks;
        //CoordinatesRequest(ts, te, 1, 1);
        SendGetUsers(1);
        
    }

    #region system functions

    public void Register(int packnum, OnReceive func)
    {
        packets[packnum] = func;
    }

    private void Register(int packnum, OnReceive func, int len)
    {
        packets[packnum] = func;
        if (len > 1)
            PacketLength[packnum] = len;
    }

    public int GetLength(byte[] data, int num, int offset)
    {
        int leng;
        if (!PacketLength.ContainsKey(num))
        {
            var rest = data.Length - offset;
            //packet not full
            if (rest < 5)
                return 1;
            leng = BitConverter.ToInt32(data, offset + 1);
        }
        else
            leng = PacketLength[num];
        //Debug.Log("GetLength " + leng);
        return leng;
    }

    int maximumPacketsPerUpdate = 50;

    public void Start()
    {
        entered = true;
    }

    public void Update()
    {     
#if UNITY_WEBGL
        Receive();
#endif
        if (instance == null || !entered)    //вход не нажат, ждем
        {
            return;
        }
        else
        {
            if (!StartConnectFlag)
            {
                StartCoroutine(Connect());
                StartConnectFlag = true;
            }
        }
        if(AuthorizationController.send)SendLogin();
        int from_connect = DateTime.Now.Subtract(connect_started).Seconds;

        if (from_connect > 7 && from_connect != DateTime.Now.Second && account_id == 0)
        {
            Exit();
        }
        var processedCount = 0;
        while (PacketQueue.Count > 0 && processedCount < maximumPacketsPerUpdate)   //если длина очереди с пакетами ==0
        {
            byte[] packetbytes;
            lock (packetqueuelock)
            {
                packetbytes = PacketQueue.Dequeue();
            }
            int offset = 0;
            int totallen = 0;
            int num = (int)packetbytes[0];
            int leng = GetLength(packetbytes, packetbytes[0], 0);

            if (leng <= packetbytes.Length)
            {
                while (offset < packetbytes.Length)
                {
                    num = (int)packetbytes[offset];
                    leng = GetLength(packetbytes, num, offset);
                    totallen += leng;
                    print("num " + num + " leng " + leng);
                    byte[] newpack = new byte[leng];
                    Array.Copy(packetbytes, offset, newpack, 0, leng);
                    offset += leng;
                    if (ShowPackets)
                    {
                        if (num != 30)    //не пакет пинга
                        {
                            //string mygg = packets[num].Method.Name + " ";       //Печатаем принятые байты!
                            string mygg = "";
                            foreach (byte b in newpack)
                            {
                                mygg += b + " ";
                            }
                            //Debug.Log("ticks " + DateTime.Now.Ticks / 10000 + " timesince " + Time.timeSinceLevelLoad +" "+ newpack.Length + " bytes packet: " + mygg);
                            Debug.Log(newpack.Length + " bytes packet: " + mygg);
                        }
                    }
                    processedCount++;
                    packets[num](newpack);//запустить OnReceive функцию
                    //print("copy "+num);
                }
            }
        }
        //Debug.Log("Update End");
    }

    public void LateUpdate()
    {
        while (SendQueue.Count > 0)
        {
            //print("SendQueue.Count " + SendQueue.Count);
            byte[] sendstring = SendQueue.Dequeue();
            //print("sendstring len " + sendstring.Length);
            byteSend(sendstring);
        }
    }

    public class StateObject
    {
        // Client socket.

        // Size of receive buffer.
        public const int BufferSize = 128000;
        // Receive buffer.
        public byte[] buffer = new byte[BufferSize];
        // Received data string.
    }

    public static Queue<byte[]> PacketQueue = new Queue<byte[]>();
    static Queue<byte[]> SendQueue = new Queue<byte[]>();
    public static bool receiveDone, connectDone, sendDone = false;

    public static StateObject state = new StateObject();
    private IEnumerator Connect()
    {
        Debug.Log("Connect");
        // Connect to a remote server.
#if UNITY_WEBGL
        var uristring = $"ws://{ServerName}:{webglPort}/ws";
        Debug.Log($"WebSocket tryconnect: {uristring}");
        WebSocket client = new WebSocket(new Uri(uristring));
        Mysocket = client;
        yield return StartCoroutine(Mysocket.Connect());
        connectDone = (Mysocket.error == null);
        if (!connectDone)
        {
            Debug.Log("WebSocket error: " + Mysocket.error);
            yield break;
        }
#else
        // Establish the remote endpoint for the socket.
        /*IPAddress ip = */
        new IPAddress(new byte[] { 127, 0, 0, 1 });
        IPHostEntry ipHostInfo = Dns.GetHostEntry(ServerName);
        //string localHost = Dns.GetHostName();
        //print("localHost " + ipHostInfo.AddressList[0]);
        //IPHostEntry ipHostInfo = Dns.GetHostEntry(localHost);

        IPAddress[] ipv4Addresses = Array.FindAll(ipHostInfo.AddressList, a => a.AddressFamily == AddressFamily.InterNetwork);
        //print("localHost v4 " + ipv4Addresses[0]);
        // IPAddress ipAddress = ipHostInfo.AddressList[0];
        IPAddress ipAddress = ipv4Addresses[0];

        //IPEndPoint remoteEP = new IPEndPoint(ip, socketPort);
        IPEndPoint remoteEP = new IPEndPoint(ipAddress, socketPort);

        // Create a TCP/IP socket.
        Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        //Socket client = new Socket(remoteEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
        print("remoteEP.AddressFamily " + AddressFamily.InterNetwork);
        print("client " + client.AddressFamily);
        Mysocket = client;
        // Connect to the remote endpoint.
        try
        {
            client.BeginConnect(remoteEP, new AsyncCallback(ConnectCallback), client);
        }
        catch (Exception e)
        {
            print(e.Message);
        }
        int num = 0;
        while (!connectDone && num < 10)
        {
            num++;
            yield return new WaitForSeconds(0.2f);
        }

        if (!connectDone)
        {
            print(socketPort + " port failed");
            yield break;
        }
#endif
        print("connected");
        connected = true;
        while (true)
        {
            if (!entered)
                break;

            while (!sendDone && connected)
            {
                yield return 1;
            }

            Receive();

            while (!receiveDone && connected)
            {
                yield return 1;
            }
            receiveDone = false;
            break;
        }
        yield break;
    }

    private static void ConnectCallback(IAsyncResult ar)
    {
        // Retrieve the socket from the state object.
        Socket client = (Socket)ar.AsyncState;
        connect_started = DateTime.Now;
        // Complete the connection.
        try
        {
            client.EndConnect(ar);
        }
        catch (Exception e)
        {
            print(e.Message);
        }

        // Signal that the connection has been made.
        connectDone = true;
    }

    private void Receive()
    {
#if UNITY_WEBGL
        CheckWebSocket();

        if (Mysocket == null)
            return;

        byte[] packet = null;
        do
        {
            packet = Mysocket.Recv();
            if (packet != null)
            {
                lock (packetqueuelock)
                {
                    PacketQueue.Enqueue(packet);
                }
                receiveDone = true;
                totalbytes += packet.Length;
            }
        }
        while (packet != null);
#else
        Mysocket.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0,
            new AsyncCallback(ReceiveCallback), state);
#endif
    }

    public static object packetqueuelock = new object();
    public static Queue<byte[]> tempqueue = new Queue<byte[]>();

    public bool CheckLength(byte[] packetbytes)
    {
        if (packetbytes.Length > 0)   //если длина очереди с пакетами ==0
        {
            int offset = 0;
            //TODO проверка на мусор
            int num = packetbytes[0];
            int leng = GetLength(packetbytes, packetbytes[0], 0);

            if (leng <= packetbytes.Length)
            {
                while (offset < packetbytes.Length)
                {
                    num = packetbytes[offset];
                    leng = GetLength(packetbytes, num, offset);
                    if (leng == 1)
                    {
                        print("Error: CheckLength: packet not complete");
                        return false;
                    }
                    //print("Checking Length of " + num + " len " + leng);
                    if (leng == 0)
                    {
                        print("CheckLength2 break! length error " + num);
                        dc = true;
                        break;
                    }
                    offset += leng;
                }
            }
            if (offset == packetbytes.Length)
                return true;
            return false;
        }
        return false;
    }

    public static int totalbytes = 0;

#if !UNITY_WEBGL
    private void ReceiveCallback(IAsyncResult ar)    //TODO если приходят данные больше длины буфера, пакеты режутся на части и складываются в обратном порядке - fix, пока что увеличим буфер
    {
        // Retrieve the state object and the client socket 
        // from the asynchronous state object.
        StateObject state = (StateObject)ar.AsyncState;

        // Read data from the remote device.
        int bytesRead = 0;
        bytesRead = Mysocket.EndReceive(ar);

        if (bytesRead > 0)
        {
            // All the data has arrived; put it in response.
            byte[] newbuf = new Byte[bytesRead];
            Array.Copy(state.buffer, newbuf, bytesRead);

            //if (ShowPackets)
            //{
            //    if (newbuf[0] != 30)    //не пакет пинга
            //    {
            //        string mygg = "";       //Печатаем принятые байты!
            //        foreach (byte b in newbuf)
            //        {
            //            mygg += b + " ";
            //        }
            //        print(newbuf.Length + " bytes: " + mygg);
            //    }
            //    totalbytes += bytesRead;
            //}

            byte[] tocheck = newbuf;
            if (tempqueue.Count > 0)
            {
                Queue<byte[]> myqueue = new Queue<byte[]>(tempqueue);
                List<byte> list = new List<byte>();
                while (myqueue.Count > 0)
                {
                    list.AddRange(myqueue.Dequeue());
                }
                list.AddRange(newbuf);
                tocheck = list.ToArray();
            }
            if (!CheckLength(tocheck))
            {
                tempqueue.Clear();
                tempqueue.Enqueue(tocheck);
            }
            else
            {
                tempqueue.Clear();
                lock (packetqueuelock)
                {
                    PacketQueue.Enqueue(tocheck);
                }
            }
            receiveDone = true;
            Receive();
        }
    }
#endif

#if UNITY_WEBGL
    private static void CheckWebSocket()
    {
        if (Mysocket != null && Mysocket.error != null)
        {
            Debug.LogError(Mysocket.error);
            instance.Exit();
        }
    }
#endif

    private static void byteSend(byte[] tosend)
    {
        if (tosend == null)
        {
            Debug.LogError("tosend null");
            return;
        }

        if (ShowPacketsSent)
        {
            byte[] newbuf = new Byte[tosend.Length];
            Array.Copy(tosend, newbuf, tosend.Length);
            if (newbuf[0] != 30)    //не пакет пинга
            {
                string mygg = "sent: ";       //Печатаем отправленные байты!
                foreach (byte b in newbuf)
                {
                    mygg += b + " ";
                }
                print(newbuf.Length + " bytes: " + mygg);
            }
        }

#if UNITY_WEBGL
        byte[] webglbuf = new Byte[tosend.Length];
        Array.Copy(tosend, webglbuf, tosend.Length);
        CheckWebSocket();
        if (Mysocket != null)
        {
            Mysocket.Send(webglbuf);
            sendDone = true;
            Debug.Log("websocket sendDone");
        }
#else
        try
        {
            //print("buffer size " + Mysocket.SendBufferSize + " tosend.Length " + tosend.Length);            
            Mysocket.BeginSend(tosend, 0, tosend.Length, 0, SendCallback, Mysocket);
        }
        catch (Exception e)
        {
            if (instance != null)
            {
                print(e.Message);
                instance.Exit();
            }
        }
#endif
    }

#if !UNITY_WEBGL
    private static void SendCallback(IAsyncResult ar)
    {
        // Retrieve the socket from the state object.
        Socket client = (Socket)ar.AsyncState;

        // Complete sending the data to the remote device.
        /*int bytesSent = */
        client.EndSend(ar);

        //print("Sent " + bytesSent + " bytes to server.");

        // Signal that all bytes have been sent.

        sendDone = true;
    }
#endif

    #endregion

    public static string GetMD5Hash(string input)
    {
        System.Security.Cryptography.MD5CryptoServiceProvider x = new System.Security.Cryptography.MD5CryptoServiceProvider();
        byte[] bs = Encoding.UTF8.GetBytes(input);
        bs = x.ComputeHash(bs);
        StringBuilder s = new StringBuilder();
        foreach (byte b in bs)
        {
            s.Append(b.ToString("x2").ToLower());
        }
        string password = s.ToString();
        return password;
    }

    /// <summary>
    /// Запрос координат с сервера, timeStart, timeEnd == DateTime.Ticks
    /// </summary>
    public void CoordinatesRequest(long timeStart, long timeEnd, byte resolution, uint locationID, uint account_id)
    {
        login_sent = true;
        Debug.Log("SendRequestCoordinates");
        List<byte> list = new List<byte>();
        list.Add(3);
        list.AddRange(BitConverter.GetBytes(account_id));
        list.AddRange(BitConverter.GetBytes(timeStart));
        list.AddRange(BitConverter.GetBytes(timeEnd));
        list.Add(resolution);
        list.AddRange(BitConverter.GetBytes(locationID));
        SendEnqueue(list.ToArray());
    }

    //companyId: 1 = Тайшет, 2 = Тестовая, 3 = Братское
    public void SendGetUsers(uint companyId)
    {
        List<byte> list = new List<byte>();
        list.Add(47);
        list.AddRange(BitConverter.GetBytes(companyId));        
        SendEnqueue(list.ToArray());
    }

    public void UsersReceive(byte[] bytedata)
    {
        print("UsersReceive");
        WorkerController.users = new List<User>();
        int read = 5;
        while (read < bytedata.Length)
        {
            var id = BitConverter.ToUInt32(bytedata, read);
            var lenname = bytedata[read+4];
            var name = Encoding.UTF8.GetString(bytedata, read+5, lenname);
            read += 5 + lenname;
            //print($"user received id {id} lenname {lenname} name {name}");
            WorkerController.users.Add(new User { id = id, name = name });
        }
        WorkerController.users_load = true;        
    }

    public void BeaconsReceive(byte[] bytedata)
    {
        int read = 5;
        while (read < bytedata.Length)
        {
            var id = BitConverter.ToUInt32(bytedata, read);
            var uuid = Encoding.UTF8.GetString(bytedata, read + 4, 36);
            var x = BitConverter.ToSingle(bytedata, read + 40);
            var y = BitConverter.ToSingle(bytedata, read + 44);
            var z = BitConverter.ToSingle(bytedata, read + 48);
            var minor = BitConverter.ToUInt16(bytedata, read + 52);
            var major = BitConverter.ToUInt16(bytedata, read + 54);
            print($"beacon received id {id} uuid {uuid} x {x} y {y} z {z} minor {minor} major {major}");
            read += 56;

            WorkerController.Beacons.Add(new Beacon { id = id, uuid = uuid, x = x, y = y, z = z, minor = minor, major = major });            
        }
        WorkerController.beacons_load = true;
    }

    public void BeaconsRequest(uint locationId)
    {
        Debug.Log("BeaconsRequest connected "+ connected);
        //if (!connected)
        //{            
        //    return;
        //}
        SendFourByteParamPacket(48, locationId);
    }

    public void SendLogin()
    {
        if (!connected)
        {
            //Debug.LogError("Couldn't connect");
            //Exit();
            return;
        }
        if (!login_sent)
        {
            login_sent = true;
            Debug.Log("SendLogin");
            string passwordToEdit = AuthorizationController.password;// "8*9Efc2%";
            string login = AuthorizationController.login;//"vsheiko";
            string pass = GetMD5Hash(passwordToEdit);

            byte[] bpass = Encoding.UTF8.GetBytes(pass);
            byte[] blogin = Encoding.UTF8.GetBytes(login);
            List<byte> list = new List<byte>();
            byte loginFlag = 1;
            int leng = (bpass.Length + blogin.Length + 6);   //+имя+длина        
            list.Add(2);
            list.AddRange(BitConverter.GetBytes(leng));
            list.Add(loginFlag);
            list.AddRange(bpass);
            list.AddRange(blogin);
            SendEnqueue(list.ToArray());
        }
    }
}