#include <thread>
#include <vector>
#include <string>
#include <Display/WidgetDisplay/ConnectingDisplay/ConnectingDisplay.hpp>
#include <Widget/Label/Label.hpp>
#include <Widget/Layout/Center/Center.hpp>
#include <Widget/Layout/Column/Column.hpp>
#include <Widget/Button/Button.hpp>
#include <Display/WidgetDisplay/ServerSelection/ServerSelection.hpp>
#include <Widget/TextField/TextField.hpp>
#include <Display/WidgetDisplay/WorldDisplay/WorldDisplay.hpp>
#include <Display/WidgetDisplay/MessageDisplay/MessageDisplay.hpp>
#include <Utilities/ServerSessionTable.hpp>
#include <World/RemoteWorld/RemoteWorld.hpp>
#include <Utilities/Socket.hpp>
#include <Display/WidgetDisplay/WorldDisplay/WorldDisplay.hpp>
#include <Display/WidgetDisplay/LoginDisplay/LoginDisplay.hpp>

void ConnectingDisplay::error(const std::string &caption, const std::string &msg)
{
    Display::setCurrent<MessageDisplay>(Material::alert, caption, msg);
};

ConnectingDisplay::ConnectingDisplay(std::shared_ptr<Authenticator> auth, std::string server)
{
    setWidget(new Center(new Label("Connecting...")));

    std::thread([this, server, auth]()
    {
        RemoteWorld::ConnectionData connectionData;
        std::string userName = auth->getUserName();

        std::string error;
        SOCKET sock = connectSocket(server, error);

        if (sock == INVALID_SOCKET)
        {
            this->m_cmdqueue.add([error](ConnectingDisplay &display)
            {
                display.error("Could not connect", error);
            });
            
            return;
        }

        connectionData.socket = sock;

        // Send a Login greeting.
        Protocol::ClientGreetingHeader clientHeader(Protocol::ClientGreetingCode::Login);
        if (!writeSocket(sock, (char*) &clientHeader, sizeof(clientHeader)))
        {
            closesocket(sock);

            this->m_cmdqueue.add([](ConnectingDisplay &display)
            {
                display.error("Could not connect", "Server disconnected.");
            });
            
            return;
        };

        // Send the login greeting payload.
        if (!auth->sendGreetingPayload(sock))
        {
            closesocket(sock);

            this->m_cmdqueue.add([](ConnectingDisplay &display)
            {
                display.error("Could not connect", "Server disconnected.");
            });
            
            return;
        };

        // Read the server greeting.
        Protocol::ServerGreetingHeader serverHeader;
        if (!readSocket(sock, (char*) &serverHeader, sizeof(serverHeader)))
        {
            closesocket(sock);

            this->m_cmdqueue.add([](ConnectingDisplay &display)
            {
                display.error("Could not connect", "Server disconnected.");
            });
            
            return;
        };

        // Check that the greeting is valid.
        if (!serverHeader.isValid())
        {
            closesocket(sock);

            this->m_cmdqueue.add([](ConnectingDisplay &display)
            {
                display.error("Server error", "Invalid server greeting.");
            });
            
            return;
        };

        // Check the greeting code.
        switch (serverHeader.code)
        {
        case Protocol::ServerGreetingCode::LoginOK:
            break;
        case Protocol::ServerGreetingCode::LoginFailed:
            closesocket(sock);

            this->m_cmdqueue.add([](ConnectingDisplay &display)
            {
                display.error("Server error", "Login protocol error.");
            });
            
            return;
        case Protocol::ServerGreetingCode::IncompatibleVersion:
            closesocket(sock);

            this->m_cmdqueue.add([serverHeader](ConnectingDisplay &display)
            {
                display.error(
                    "Incompatible server",
                    formatString(
                        "Your version: $+\nServer version: $+",
                        Version::ofThisBuild,
                        serverHeader.version
                    )
                );
            });
            
            return;
        case Protocol::ServerGreetingCode::SessionExpired:
            closesocket(sock);

            this->m_cmdqueue.add([server, userName](ConnectingDisplay &)
            {
                Display::setCurrent<LoginDisplay>("Session expired. Please enter your password.", server, userName);
            });
            
            return;
        case Protocol::ServerGreetingCode::WrongUsernameOrPassword:
            closesocket(sock);

            this->m_cmdqueue.add([](ConnectingDisplay &display)
            {
                display.error("Could not log in", "Wrong username or password.");
            });
            
            return;
        case Protocol::ServerGreetingCode::UserBanned:
            closesocket(sock);

            this->m_cmdqueue.add([](ConnectingDisplay &display)
            {
                display.error("Could not log in", "You are banned from this server.");
            });
            
            return;
        case Protocol::ServerGreetingCode::UserPending:
            closesocket(sock);

            this->m_cmdqueue.add([](ConnectingDisplay &display)
            {
                display.error("Could not log in", "Your account is awaiting approval from an admin.");
            });
            
            return;
        case Protocol::ServerGreetingCode::AlreadyLoggedIn:
            closesocket(sock);

            this->m_cmdqueue.add([](ConnectingDisplay &display)
            {
                display.error("Could not log in", "You are already logged into this server.\nLog off being trying to connect from another machine.");
            });
            
            return;
        default:
            closesocket(sock);

            this->m_cmdqueue.add([serverHeader](ConnectingDisplay &display)
            {
                display.error("Server error", formatString("Unrecognized greeting code: $+", static_cast<int>(serverHeader.code)));
            });
            
            return;
        };

        // Receive the session ID.
        UserDB::SessionID sid;
        if (!readSocket(sock, (char*) &sid, sizeof(sid)))
        {
            closesocket(sock);

            this->m_cmdqueue.add([](ConnectingDisplay &display)
            {
                display.error("Could not connect", "Server disconnected.");
            });
            
            return;
        };

        // Update the session ID in `servers.dat`.
        setServerSession(server, auth->getUserName(), sid);

        // Receive the weather gen node.
        if (!readSocket(sock, (char*) &connectionData.weatherNode, sizeof(connectionData.weatherNode)))
        {
            closesocket(sock);

            this->m_cmdqueue.add([](ConnectingDisplay &display)
            {
                display.error("Could not connect", "Server disconnected.");
            });
            
            return;
        }

        // Receive the world time.
        if (!readSocket(sock, (char*) &connectionData.worldTime, 8))
        {
            closesocket(sock);

            this->m_cmdqueue.add([](ConnectingDisplay &display)
            {
                display.error("Could not connect", "Server disconnected.");
            });
            
            return;
        }

        // Receive the player entity ID and data size.
        uint64_t playerEntityID;
        uint32_t playerDataSize;
        if (!readSocket(sock, (char*) &playerEntityID, 8) || !readSocket(sock, (char*) &playerDataSize, 4))
        {
            closesocket(sock);

            this->m_cmdqueue.add([](ConnectingDisplay &display)
            {
                display.error("Could not connect", "Server disconnected.");
            });
            
            return;
        };

        connectionData.playerEntityID = playerEntityID;
        connectionData.userName = auth->getUserName();

        // Load the player data.
        char *playerData = new char[playerDataSize];
        if (!readSocket(sock, playerData, playerDataSize))
        {
            delete[] playerData;
            closesocket(sock);

            this->m_cmdqueue.add([](ConnectingDisplay &display)
            {
                display.error("Could not connect", "Server disconnected.");
            });
            
            return;
        };

        connectionData.playerData = playerData;
        connectionData.playerDataSize = playerDataSize;

        // Receive the block table
        for (;;)
        {
            uint8_t sizeByte;
            if (!readSocket(sock, (char*) &sizeByte, 1))
            {
                delete[] playerData;
                closesocket(sock);

                this->m_cmdqueue.add([](ConnectingDisplay &display)
                {
                    display.error("Could not connect", "Server disconnected.");
                });
                
                return;
            };

            if (sizeByte == 0)
            {
                break;
            };

            size_t strSize = sizeByte;
            char *strData = new char[strSize+1];

            if (!readSocket(sock, strData, strSize))
            {
                delete[] playerData;
                closesocket(sock);

                this->m_cmdqueue.add([](ConnectingDisplay &display)
                {
                    display.error("Could not connect", "Server disconnected.");
                });
                
                return;
            };

            strData[strSize] = 0;

            connectionData.blockTable.push_back(std::string(strData));

            delete[] strData;
        };

        // Receive the time-of-day.
        uint32_t timeOfDay;
        if (!readSocket(sock, (char*) &timeOfDay, 4))
        {
            delete[] playerData;
            closesocket(sock);
            
            this->m_cmdqueue.add([](ConnectingDisplay &display)
            {
                display.error("Could not connect", "Server disconnected.");
            });
            
            return;
        };
        connectionData.timeOfDay = timeOfDay;

        // Receive the belt views.
        uint32_t beltDataSize;
        if (!readSocket(sock, (char*) &beltDataSize, 4))
        {
            delete[] playerData;
            closesocket(sock);
            
            this->m_cmdqueue.add([](ConnectingDisplay &display)
            {
                display.error("Could not connect", "Server disconnected.");
            });
            
            return;
        };

        connectionData.beltData.resize(beltDataSize);
        if (!readSocket(sock, (char*) connectionData.beltData.data(), beltDataSize))
        {
            delete[] playerData;
            closesocket(sock);
            
            this->m_cmdqueue.add([](ConnectingDisplay &display)
            {
                display.error("Could not connect", "Server disconnected.");
            });
            
            return;
        };

        // Begin the world.
        this->m_cmdqueue.add([connectionData](ConnectingDisplay&)
        {
            std::shared_ptr<RemoteWorld> world = std::make_shared<RemoteWorld>(connectionData);
            Display::setCurrent<WorldDisplay>(world, world->getOurPlayer());
        });
    }).detach();
};

void ConnectingDisplay::update()
{
    WidgetDisplay::update();
    m_cmdqueue.processAllCommands(*this);
};