using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Net.Sockets;

using UnityEngine;
using System.Collections;
using UnityEngine.Events;
using UnityEngine.UI;

namespace Assets.Scripts.Network
{
    public class Network : MonoBehaviour
    {
        public static Network _ = null;

        public const String version = "v 1.0.0";
        private string ip = "192.168.8.108";
        int port = 4564;

#if UNITY_WEBGL
        WebSocket wsock;
        ByteArray wsdata = new ByteArray();
#else
        TcpClient client;
        NetworkStream stream;
#endif

        float ConnectTimeout;

        [System.Serializable]
        public class OnPingEvent : UnityEvent<Stats> { }
        public OnPingEvent onPingEvent = new OnPingEvent();
        public UnityEvent OnConnectedEvent = new UnityEvent();
        public UnityEvent OnDisconnectedEvent = new UnityEvent();

        public InputField IpInputField;
        public Button NetworkJoinButton;

        public enum NetworkStateEnum
        {
            nsDisconnected,
            nsConnecting,
            nsConnected,
            nsDisconnecting,
        };

        NetworkStateEnum NetworkState = NetworkStateEnum.nsDisconnected;
        NetworkStateEnum PrevNetworkState = NetworkStateEnum.nsDisconnected;

        bool ShouldConnect = false;

        public void Connect(string _ip, int _port)
        {
            ip = _ip;
            port = _port;
            ShouldConnect = true;
        }

        public void Disconnect()
        {
            Debug.Log("disconnecting...");
            ShouldConnect = false;
#if UNITY_WEBGL
            if( wsock != null )
            {
                wsock.Close();
                wsock = null;
            }
#else
            if (client != null)
            {
                if (client.Connected)
                {
                    client.Close();
                }
            }
#endif
        }

        // Use this for initialization
        void Start()
        {
            _ = this;
        }

        float pingRecv = -1;
        float pingDelay = 0;
        float pingSent = 0;
        float pingRoundtrip = 0;

        ByteArray outgoingData = new ByteArray();
        ByteArray incomingData = new ByteArray();
        UInt16 PacketSize = 0;
        private uint uid;
        private string loginID;
        private string token;
        private bool chatIsBlocked;
        private bool promoteInvite;
        private int allowScreenshotDebug;
        private bool allowMobileDailyAds;
        private bool allowMobilePopupAds;

#if UNITY_WEBGL
        IEnumerator WSConnect()
        {
            yield return StartCoroutine(wsock.Connect());
            NetworkState = NetworkStateEnum.nsConnected;
            ShouldConnect = false;
        }
#endif

        // Update is called once per frame
        void Update()
        {
            switch (NetworkState)
            {
                case NetworkStateEnum.nsConnecting:
                    ConnectTimeout -= Time.deltaTime;
                    if (ConnectTimeout < 0)
                    {
                        try
                        {
#if UNITY_WEBGL
                            wsock.Close();
#else
                            client.Close();
#endif
                        }
                        catch (Exception)
                        {

                        }
                    }
                    break;
                case NetworkStateEnum.nsDisconnected:
#if UNITY_WEBGL
                    InitNetwork();
#endif
                    if (ShouldConnect)
                    {
                        //initiate connect
#if UNITY_WEBGL
                        if( wsock == null )
                        {
                            Debug.Log("connecting...");
                            wsock = new WebSocket(new Uri("ws://local.fufla.eu/ws/h/4564"));
                            wsdata.clear();
                            StartCoroutine(WSConnect());
                        }
#else
                        if (client == null)
                        {
                            client = new TcpClient();
                        }
                        NetworkState = NetworkStateEnum.nsConnecting;
                        ConnectTimeout = 10;
                        client.BeginConnect(ip, port, asyncResult => {
                            try
                            {
                                client.EndConnect(asyncResult);
                                Debug.Log("socket connected");
                                stream = client.GetStream();
                                outgoingData.clear();
                                incomingData.clear();
                                PacketSize = 0;
                                pingRecv = -1;
                                NetworkState = NetworkStateEnum.nsConnected;
                                ShouldConnect = false;
                                OnConnectedEvent.Invoke();
                            }
                            catch (Exception)
                            {
                                try
                                {
                                    client.Close();
                                    client = null;
                                }
                                catch (Exception)
                                {

                                }
                                Debug.Log("socket closed before connected");
                                NetworkState = NetworkStateEnum.nsDisconnected;
                            }
                        }, this);
#endif
                    }
                    break;
                case NetworkStateEnum.nsConnected:
                    //send regular pings and receive data
                    try
                    {
#if UNITY_WEBGL
                        int prevpos = wsdata.position;
                        wsdata.position = wsdata.length;

                        byte[] data = wsock.Recv();
                        while( data != null )
                        {
                            wsdata.writeBytes(data);
                            data = wsock.Recv();
                        }

                        wsdata.position = prevpos;

                        while( wsdata.bytesAvailable() > 0 )
                        {
                            if (PacketSize == 0)    
                            {
                                if (wsdata.bytesAvailable() >= 2)
                                {
                                    PacketSize = wsdata.readUnsignedShort();
                                    PacketSize -= 2;
                                }
                                else break;//not enough bytes
                            }
                            if (PacketSize > 0)
                            {
                                if (wsdata.bytesAvailable() >= PacketSize)
                                {
                                    incomingData.clear();
                                    incomingData.ensure(PacketSize);
                                    wsdata.readBytes(incomingData, 0, PacketSize);
                                   // Debug.Log("got whole packet");
                                    if (wsdata.bytesAvailable() == 0)
                                        wsdata.clear();

                                    incomingData.position = 0;
                                    incomingData.length = PacketSize;

                                    //process packet
                                    while (incomingData.bytesAvailable() > 0)
                                    {
                                        ProcessCommand();
                                    }
                                    incomingData.clear();
                                    PacketSize = 0;
                                }
                                else break;//wait more
                            }
                        }
#else
                        while (client.Available > 0)
                        {
                            if (PacketSize == 0)
                            {
                                if (client.Available >= 2)
                                {
                                    byte[] tmp = new byte[2];
                                    if (stream.Read(tmp, 0, 2) != 2)
                                        throw new Exception();
                                    PacketSize = BitConverter.ToUInt16(tmp, 0);
                                    PacketSize -= 2;
                                }
                                else break;//not enough bytes
                            }
                            if (PacketSize > 0)
                            {
                                if (client.Available >= PacketSize)
                                {
                                    incomingData.clear();
                                    incomingData.ensure(PacketSize);
                                    int bytesRead = stream.Read(incomingData.arr, 0, PacketSize);
                                    if (bytesRead != PacketSize)
                                        throw new Exception();
                                    incomingData.position = 0;
                                    incomingData.length = PacketSize;
                                    //process packet
                                    while (incomingData.bytesAvailable() > 0)
                                    {
                                        ProcessCommand();
                                    }
                                    incomingData.clear();
                                    PacketSize = 0;
                                }
                                else break;//wait more
                            }
                        }
#endif
                        if (pingRecv == -1)
                        {
                            pingDelay = 0;
                            pingRecv = 0;
                            pingSent = Time.time;
                            outgoingData.writeByte((byte)Messages.op_ping);
                            Send();
                        }
                        else
                        {
                            pingDelay += Time.deltaTime;
                            if (pingDelay > 5)
                            {
                                this.pingRecv = -1;//send another ping
                            }
                        }
                    }
                    catch (Exception e)
                    {
                        Debug.Log("socket disconnected on read " + e.ToString());
                        try
                        {
#if UNITY_WEBGL
                            wsock.Close();
                            wsock = null;
#else
                            client.Close();
                            client = null;
#endif
                        }
                        catch (Exception)
                        {

                        }
                        NetworkState = NetworkStateEnum.nsDisconnected;
                    }
                    break;
            }
            if (PrevNetworkState != NetworkState)
            {
                switch (NetworkState)
                {
                    case NetworkStateEnum.nsConnecting:
                        break;
                    case NetworkStateEnum.nsConnected:
                        //outgoingData.writeByte( (byte)Messages.op_ping );
                        //Send();

                        //destroy existing world
                        if (World.currentWorld != null)
                        {
                            World.currentWorld.DestroyWorld();
                        }
                        else World.currentWorld = new World();

                        outgoingData.writeByte((byte)Messages.op_login);
                        outgoingData.writeUTF(version);
                        Send();
                        break;
                    case NetworkStateEnum.nsDisconnected:
                        OnDisconnectedEvent.Invoke();
                        break;
                }
                PrevNetworkState = NetworkState;
            }
        }

        private void FailedToAuth()
        {
            ShouldConnect = false;
        }

        public void SendMove( Vector2 direction, float Angle )
        {
            if( NetworkState == NetworkStateEnum.nsConnected )
            {
                if ((direction.x == 0) && (direction.y == 0))
                {
                    //stopped
                    outgoingData.writeByte((byte)Messages.op_move_stop);
                    outgoingData.writeFloat(Angle);
                    Send();
                }
                else
                {
                    direction.Normalize();
                    outgoingData.writeByte((byte)Messages.op_move_dir);
                    outgoingData.writeFloat(direction.x);
                    outgoingData.writeFloat(direction.y);
                    outgoingData.writeFloat(Angle);
                    Send();
                }
            }
        }

        private void Send()
        {
            if (outgoingData.length > 0)
            {
                try
                {
#if UNITY_WEBGL
                    //Debug.Log("sending data out " + outgoingData.length + " bytes");
                    wsock.Send(BitConverter.GetBytes((ushort)outgoingData.length));
                    byte[] tmp = new byte[outgoingData.length];
                    for (int k = 0; k < outgoingData.length; k++)
                        tmp[k] = outgoingData.arr[k];
                    wsock.Send(tmp);
#else
                    stream.Write(BitConverter.GetBytes((ushort)outgoingData.length), 0, 2);
                    stream.Write(outgoingData.arr, 0, outgoingData.length);
                    stream.Flush();
#endif
                    outgoingData.clear();
                }
                catch (Exception)
                {
                    try
                    {
#if UNITY_WEBGL
                        wsock.Close();
                        wsock = null;
#else
                        client.Close();
#endif
                    }
                    catch (Exception)
                    {

                    }
                }
            }
        }

        public class Stats
        {
            public ushort users;
            public ushort online_friends;
            public ushort online_buddies;
            public ushort games;
            public uint time_till_reset;
            public uint uid;
        }

        void onPing()
        {
            //trace('onping');
            pingRecv = Time.time;
            pingRoundtrip = pingRecv - pingSent;
            pingDelay = 4;//wait one more second to send out next ping
                          /*
                                      var stats = new Stats();
                                      stats.users = incomingData.readUnsignedShort();
                                      stats.online_friends = incomingData.readUnsignedShort();
                                      stats.online_buddies = incomingData.readUnsignedShort();
                                      stats.games = incomingData.readUnsignedShort();
                                      stats.time_till_reset = incomingData.readUnsignedInt();
                                      stats.uid = incomingData.readUnsignedInt();
                                      onPingEvent.Invoke(stats);
                                      */
            //Debug.Log("Ping: " + pingRoundtrip);            

            //this.in_func = 'onStats';
            //this.callbackObject.onStats(stats);
            //this.in_func = '';
        }

        public class ServerMessage
        {
            public byte msg_type;
            public string msg_text;
            public string msg_header;
            public uint some_value;
        }

        private void ProcessCommand()
        {
            Messages m = (Messages)incomingData.readByte();
            switch (m)
            {
                case Messages.op_ping:
                    onPing();
                    break;
                case Messages.op_login:
                    onLogin();
                    break;
                case Messages.op_object_removed:
                    //logged in somewhere else - disconnect but allow to reconnect with button
                    {
                        int id = incomingData.readInt();
                        for( int k = 0; k < World.currentWorld.Objects.Count; k++ )
                        {
                            if( World.currentWorld.Objects[k].ID == id )
                            {
                                WorldObject wo = World.currentWorld.Objects[k];
                                World.currentWorld.Objects.RemoveAt(k);
                                wo.DeInstantiate();
                                break;
                            }
                        }
                    }
                    break;
                case Messages.op_object_added:
                    loadWorldObject();
                    break;
                case Messages.op_update_player_pos:
                    {
                        uint cnt = incomingData.readUnsignedInt();
                        float moveTime = incomingData.readFloat();
                        while( cnt-- != 0 )
                        {
                            int id = incomingData.readInt();
                            WorldObject wo = World.currentWorld.getObjectByID(id);
                            Player pl = (Player)wo;

                            PlayerUpdate pu;
                            pu.Time = moveTime;

                            pu.Position = new Vector2(incomingData.readFloat(), incomingData.readFloat());
                            pu.Orientation = incomingData.readFloat();
                            pu.Direction = new Vector2(incomingData.readFloat(), incomingData.readFloat());

                            pl.Updates.Add(pu);
                        }
                    }
                    break;
                case Messages.op_notification:
                    {
                        int id = incomingData.readInt();
                        byte event_Type = incomingData.readByte();
                        Manager._.WorldEvent.Invoke(World.currentWorld.getObjectByID( id ), (Manager.WorldEventTypes)event_Type);
                    }
                    break;
                case Messages.op_double_notification:
                    {
                        int id = incomingData.readInt();
                        int id2 = incomingData.readInt();
                        byte event_Type = incomingData.readByte();
                        if( event_Type >= 10 )
                        {
                            Manager._.WorldDouble2Event.Invoke(World.currentWorld.getObjectByID(id), id2, (Manager.WorldEventTypes)event_Type);
                        }
                        else
                        {
                            Manager._.WorldDoubleEvent.Invoke(World.currentWorld.getObjectByID(id), World.currentWorld.getObjectByID(id2), (Manager.WorldEventTypes)event_Type);
                        }
                    }
                    break;
                case Messages.op_energy:
                    {                        
                        World.currentWorld.Me.FlashLight.Level = incomingData.readFloat();
                        World.currentWorld.MyHouse.Energy.Level = incomingData.readFloat();
                        World.currentWorld.RemoteWorldTime = incomingData.readFloat();
                        Debug.Log("energy update " + World.currentWorld.Me.FlashLight.Level + " " + World.currentWorld.MyHouse.Energy.Level);
                    }   
                    break;
                default:
                    Debug.Log("Message not handled: " + m);
                    throw new NotImplementedException();
            }
        }

        private Color readColor()
        {
            uint col = incomingData.readUnsignedInt();
            uint r, g, b, a;
            b = col & 0xFF;
            g = (col >> 8) & 0xFF;
            r = (col >> 16) & 0xFF;
            a = (col >> 24) & 0xFF;
            return new Color32((byte)r, (byte)g, (byte)b, (byte)a);
        }

        private DrainableResource readDrainable()
        {
            DrainableResource dr = new DrainableResource();
            dr.Capacity = incomingData.readFloat();
            dr.DrainRate = incomingData.readFloat();
            dr.Level = incomingData.readFloat();
            return dr;
        }

        private WorldObject readWorldObject()
        {
            WorldObject.WorldObjectType tp = (WorldObject.WorldObjectType)incomingData.readByte();
            WorldObject wo;

            switch (tp)
            {
                case WorldObject.WorldObjectType.wotPlayer:
                    {
                        wo = new Player();

                        ((Player)wo).PlayerColor = readColor();
                        ((Player)wo).FlashLight = readDrainable();
                        ((Player)wo).CrystalCapacity = incomingData.readByte();
                        ((Player)wo).Speed = incomingData.readFloat();
                    }
                    break;
                case WorldObject.WorldObjectType.wotHouse:
                    {
                        wo = new House();
                        ((House)wo).Color = readColor();
                        ((House)wo).Owner = incomingData.readInt();
                        ((House)wo).PlayerDetectRangeSqr = incomingData.readFloat();
                        ((House)wo).Energy = readDrainable();
                    }
                    break;
                case WorldObject.WorldObjectType.wotCrystal:
                    {
                        wo = new Crystal();
                        ((Crystal)wo).Energy = incomingData.readFloat();
                        ((Crystal)wo).CaptureRadiusSqr = incomingData.readFloat();
                    }
                    break;
                case WorldObject.WorldObjectType.wotLightHouse:
                    {
                        wo = new LightHouse();
                        ((LightHouse)wo).LightRadiusSqr = incomingData.readFloat();
                        ((LightHouse)wo).AcquireRadiusSqr = incomingData.readFloat();
                        ((LightHouse)wo).LighHouseColor = readColor();
                        ((LightHouse)wo).OwnerID = incomingData.readInt();
                        //TODO: fetch player and assign if not 0
                    }
                    break;
                default:
                    //do nothing
                    wo = new WorldObject();
                    break;
            }

            wo.Position = new Vector2(incomingData.readFloat(), incomingData.readFloat());
            wo.Orientation = incomingData.readFloat();
            wo.Scale = incomingData.readFloat();
            wo.PrefabID = incomingData.readInt();
            wo.ID = incomingData.readInt();
            wo.Type = tp;

            return wo;
        }

        int retries = 0;
        private void onLogin()
        {
            World.MyID = incomingData.readInt();

            if (World.MyID == 0)
            {
                Debug.Log("Failed to log in");
            }
            else
            {
                Debug.Log("logged in - " + World.MyID);

                WorldConfig.HouseRechargeFlashlightRate = incomingData.readFloat();
                WorldConfig.HouseRechargeEnemyFlashlightRate = incomingData.readFloat();
                WorldConfig.HouseDischargeRate = incomingData.readFloat();
                WorldConfig.FlashLightDischargeRate = incomingData.readFloat();

                World.currentWorld.RemoteWorldTime = incomingData.readFloat();

                loadWorldObjectArray();
            }
        }

        private void loadWorldObjectArray()
        {
            int cnt = incomingData.readUnsignedShort();
            while( cnt-- != 0 )
            {
                loadWorldObject();
            }
        }

        void loadWorldObject()
        {
            WorldObject wo = readWorldObject();
            World.currentWorld.Objects.Add(wo);
            wo.Instantiate(Manager._);

            if (wo.ID == World.MyID)
            {
                World.currentWorld.Me = (Player)wo;
                Manager._.ReInitCamera();
            }
            if ((wo.Type == WorldObject.WorldObjectType.wotHouse)) {
                House house = (House)wo;
                float h, s, v;
                Color.RGBToHSV(house.Color, out h, out s, out v);
                Color adjustedColor = Color.HSVToRGB(h, s/3, Math.Min(v * 2, 1f));
                house.AssignedGameObject.GetComponent<LightColor>().SetLightColor(Color.clear, adjustedColor);
                if(house.Owner == World.MyID) {
                    World.currentWorld.MyHouse = (House)wo;
                }
            }
                
            if (wo.Type == WorldObject.WorldObjectType.wotLightHouse) {
                LightHouse lightHouse = (LightHouse)wo;
                BeaconBehaviour behaviour = wo.AssignedGameObject.GetComponent<BeaconBehaviour>();
                behaviour.SetType(lightHouse.ID % 7);
            }
        }

        public enum Messages
        {
            op_ping=0,
            op_login=1,
            op_object_removed=2,
            op_object_added=3,
            op_object_added_arr=4,
            op_move_stop=5,
            op_move_dir=6,
            op_update_player_pos=7,
            op_notification=8,
            op_energy=9,
            op_double_notification=10,
        }

        public void InitNetwork() {
            ip = this.IpInputField.text; //TODO
            this.IpInputField.gameObject.SetActive(false);
            this.NetworkJoinButton.gameObject.SetActive(false);
            ShouldConnect = true;
        }
    }
}
