#include "libs/enet/include/enet.h"

#include "network/Client.h"
#include "network/ENet.h"

static_assert(sizeof(enet_uint16) == sizeof(Client::Port),
              "client port has wrong type");

static ENetHost* client = nullptr;
static ENetPeer* connection = nullptr;
static int connectTicks = 0;
static int connectTimeoutTicks = 0;
static int disconnectTicks = 0;
static int disconnectTimeoutTicks = 0;
static Client::OnConnect onConnect = []() {};
static Client::OnDisconnect onDisconnect = []() {};
static Client::OnPacket onPacket = [](InPacket&) {};

Error Client::start() {
    if(client != nullptr) {
        return {"already started"};
    } else if(ENet::add()) {
        return {"cannot initialize enet"};
    }
    client = enet_host_create(nullptr, 1, 2, 0, 0);
    if(client == nullptr) {
        ENet::remove();
        return {"cannot create enet client host"};
    }
    return {};
}

void Client::stop() {
    if(connection != nullptr) {
        onDisconnect();
        enet_peer_disconnect_now(connection, 0);
        connection = nullptr;
    }
    if(client != nullptr) {
        enet_host_destroy(client);
        ENet::remove();
        client = nullptr;
    }
    connectTicks = 0;
    disconnectTicks = 0;
}

Error Client::connect(const char* server, Port port, int timeoutTicks) {
    if(client == nullptr) {
        return {"client not started"};
    } else if(connection != nullptr) {
        return {"connection already exists"};
    }

    ENetAddress address;
    memset(&address, 0, sizeof(ENetAddress));
    enet_address_set_host(&address, server);
    address.port = port;

    connection = enet_host_connect(client, &address, 3, 0);
    if(connection == nullptr) {
        return {"cannot create connection"};
    }
    connectTicks = 1;
    connectTimeoutTicks = timeoutTicks;
    return {};
}

void Client::disconnect(int timeoutTicks) {
    if(connection == nullptr) {
        return;
    }
    connectTicks = 0;
    enet_peer_disconnect(connection, 0);
    disconnectTicks = 1;
    disconnectTimeoutTicks = timeoutTicks;
}

void Client::send(OutPacket& p, PacketType pt) {
    if(client != nullptr && connection != nullptr && connectTicks < 0) {
        constexpr enet_uint32 flags[] = {ENET_PACKET_FLAG_RELIABLE, 0,
                                         ENET_PACKET_FLAG_UNSEQUENCED};
        int index = static_cast<int>(pt);
        enet_peer_send(
            connection, index,
            enet_packet_create(p.buffer, p.buffer.getLength(), flags[index]));
    }
}

void Client::tick() {
    if(client == nullptr) {
        return;
    }
    ENetEvent e;
    while(enet_host_service(client, &e, 0) > 0) {
        switch(e.type) {
            case ENET_EVENT_TYPE_CONNECT:
                connectTicks = -1;
                onConnect();
                break;
            case ENET_EVENT_TYPE_DISCONNECT_TIMEOUT:
            case ENET_EVENT_TYPE_DISCONNECT:
                disconnectTicks = 0;
                onDisconnect();
                connection = nullptr;
                break;
            case ENET_EVENT_TYPE_NONE: return;
            case ENET_EVENT_TYPE_RECEIVE:
                InPacket in(e.packet->data, e.packet->dataLength);
                onPacket(in);
                enet_packet_destroy(e.packet);
                break;
        }
    }
    if(connectTicks >= 1 && ++connectTicks > connectTimeoutTicks) {
        connectTicks = 0;
        disconnect(connectTimeoutTicks);
    }
    if(disconnectTicks >= 1 && ++disconnectTicks > disconnectTimeoutTicks) {
        disconnectTicks = 0;
        onDisconnect();
        if(connection != nullptr) {
            enet_peer_reset(connection);
            connection = nullptr;
        }
    }
}

void Client::setConnectHandler(OnConnect oc) {
    onConnect = oc;
}

void Client::setDisconnectHandler(OnDisconnect od) {
    onDisconnect = od;
}

void Client::setPacketHandler(OnPacket op) {
    onPacket = op;
}

void Client::resetHandler() {
    onConnect = []() {};
    onDisconnect = []() {};
    onPacket = [](InPacket&) {};
}

bool Client::isConnecting() {
    return connectTicks >= 1;
}

bool Client::isConnected() {
    return connectTicks < 0;
}