#include "Address.hpp"
#include "Entity.pb.h"
#include "Login.pb.h"
#include "PlayerMove.pb.h"
#include "ProtobufMessages.hpp"
#include "WorldState.pb.h"
#include "message_protocol/MessageEndpoint.hpp"
#include "runtime/LockStep.hpp"

#include <chrono>
#include <cstdlib>
#include <memory>
#include <semaphore>
#include <thread>
#include <vector>
#include <glm/glm.hpp>

static std::unique_ptr<tw::msg::MessageEndpoint> create_endpoint() {
    auto endpoint_r = tw::msg::MessageEndpoint::create();
    if(!endpoint_r) {
        throw std::runtime_error("Failed to create the endpoint: " + endpoint_r.error().message());
    }

    return std::move(endpoint_r.value());
}

static tw::msg::MessageConnection* connect_to_server(tw::msg::MessageEndpoint* endpoint,
                                                     tw::net::Address          address) {
    auto server_r = endpoint->connect(address.ip_string(), address.port());
    if(!server_r) {
        throw std::runtime_error("Failed to connect to server: " + server_r.error().message());
    }

    return server_r.value();
}

class MockClient {
    std::unique_ptr<tw::msg::MessageEndpoint> m_endpoint;
    tw::msg::MessageConnection*               m_server;
    tw::ProtobufMessages                      m_messages;

    tw::LockStep m_lock_step;

    uint32_t m_frame_idx;
    bool m_is_running;

    uint32_t m_entity_id;
    bool m_is_connected;

    std::binary_semaphore m_connected_semaphore;

    glm::vec3 m_velocity;

public:
    MockClient(tw::net::Address address, const std::string& name) :
        m_endpoint(create_endpoint()),
        m_server(connect_to_server(m_endpoint.get(), address)),
        m_messages(m_endpoint.get()),
        m_lock_step(20),
        m_is_running(true),
        m_is_connected(false),
        m_connected_semaphore(0)
    {
        m_messages.set_handler<mmo::WorldStateMessage>(
            [this](tw::msg::PeerId, const mmo::WorldStateMessage& mesg) { });

        m_messages.set_handler<mmo::EntitySpawnMessage>(
            [this](tw::msg::PeerId, const mmo::EntitySpawnMessage& mesg) { });

        m_messages.set_handler<mmo::LoginResponse>(
            [this](tw::msg::PeerId, const mmo::LoginResponse& mesg) {
                if(!m_is_connected) {
                    m_is_connected = true;
                    m_entity_id = mesg.entity_id();
                    m_connected_semaphore.release();
                }
            });
    }

    void run() {
        while(!m_server->is_established()) {
            m_endpoint->update();
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }

        mmo::PlayerMoveMessage player_move_mesg;

        float value;

        while(m_is_running) {
            if(m_lock_step.wait_for_next_step()) {
                continue;
            }

            m_endpoint->update();
            value += (float)std::rand() / RAND_MAX;

            m_velocity.x = glm::sin(value);
            m_velocity.z = glm::cos(value);

            m_velocity = glm::normalize(m_velocity);

            player_move_mesg.set_frame_idx(m_frame_idx);
            mmo::PlayerInput* player_input = new mmo::PlayerInput();
            player_input->set_x(m_velocity.x);
            player_input->set_y(0);
            player_input->set_z(m_velocity.z);
            player_move_mesg.set_allocated_input(player_input);
            auto r = m_messages.send(m_server, player_move_mesg, false);
            m_frame_idx++;
        }
    }
};

int main() {
    const int NUM_CLIENTS = 10;
    tw::net::Address address = {"127.0.0.1", 8101};

    std::vector<std::thread> threads;
    for(uint32_t i = 0; i < NUM_CLIENTS; i++) {
        threads.push_back(std::thread([address, i]() {
            try {
                MockClient client(address, "Player" + std::to_string(i));
                client.run();
            } catch (const std::exception& e) {
                spdlog::error("Client {} failed: {}", i, e.what());
            }
        }));
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }

    for(uint32_t i = 0; i < NUM_CLIENTS; i++) {
        threads[i].join();
    }

    return 0;
}
