#include <string.h>

#include "ConfigPanel.h"
#include "DaemonThreadManager.h"
#include "OriginTimestamp.h"
#include "TenkinetThread.h"
#include "timestamp.h"

#define NTP_INTERVAL_MS 1000
#define NTP_TRIES 3

QMutex TenkinetThread::mutex;

List TenkinetThread::devices;

struct Context {

    enum State {

        ZERO,
        DISCONNECTED,
        CONNECTING,
        CONNECTED,

    };

    State state = ZERO;
    TenkinetProvider *provider = nullptr;
    TenkinetClient tenkinetClient = nullptr;
    NTP_Client ntpClient = nullptr;

    NTP_Result ntpResult;
    int64_t origin;

    // Set by external thread; lock mutex before accessing
    uint32_t interval_ms = 0;
    bool subscribed = false;
};

struct Command {

    enum Code {

        NONE = 0,
        ADD,
        REMOVE,
        CONNECT,
        DISCONNECT,
        LIST,
        SUBSCRIBE,
        UNSUBSCRIBE,
        POLL,

    };

    Code code;
    Context *context;

    Command(Code code, void *context) : code(code), context((Context *)context) {}
};

TenkinetThread::TenkinetThread()
    : callbacks({connectionCallback, listCallback, dataCallback, deviceStatusCallback, nullptr}) {
    list_init(&devices);
    list_grow(&devices, 16); // TODO remove after list_reserve() is used in tenkinet.c

    DaemonThreadManager::instance().addThread(this);
}

TenkinetThread::~TenkinetThread() { list_clear(&devices); }

void TenkinetThread::run() {

    // Initialize tenkinet and ntp modules

    {
        QMutexLocker locker(&mutex);

        if (state != INIT) {
            return;
        }

        memset(&seldata, 0, sizeof(seldata));
        seldata.timestamp = timestamp_now();

        if (tenkinet_init(&seldata)) {
            return;
        }

        if (ntp_init(&seldata)) {
            tenkinet_exit();
            return;
        }

        state = RUNNING;
    }

    // Main loop until shutdown() is called

    struct timeval tv;

    while (1) {

        tv.tv_sec = 1;
        tv.tv_usec = (TIMESTAMP_ONE_SECOND / 10);

        if (select_helper(&seldata, &tv) < 0) {
            break;
        }

        ntp_process();
        tenkinet_process();

        if (processCommands()) {
            break;
        }
    }

    // Clean up and terminate thread

    ntp_exit();
    tenkinet_exit();

    mutex.lock();
    state = INIT;
    mutex.unlock();
}

int TenkinetThread::processCommands() {

    QMutexLocker locker(&mutex);

    if (state == SHUTDOWN) {
        commands.clear();
        return 1;
    }

    for (int i = 0; i < commands.size(); i++) {

        Command *command = (Command *)commands[i];
        Context *context = command->context;

        switch (command->code) {

        case Command::Code::ADD: {
            TenkinetProvider *provider = context->provider;
            callbacks.user_data = context;

            QString host = provider->getServerHost();
            QByteArray host_ba = host.toLocal8Bit();
            char *host_s = host_ba.data();

            QString port = provider->getServerPort();
            QByteArray port_ba = port.toLocal8Bit();
            char *port_s = port_ba.data();

            bool success = false;

            context->tenkinetClient = tenkinet_client_new(host_s, port_s, &callbacks);
            context->ntpClient = ntp_client_new(host_s, NTP_PORT_STR, ntpCallback, context);

            if (context->tenkinetClient != nullptr && context->ntpClient != nullptr) {
                context->state = Context::State::DISCONNECTED;
            }
        } break;

        case Command::Code::REMOVE:
            if (context->ntpClient) {
                ntp_client_delete(context->ntpClient);
            }
            if (context->tenkinetClient) {
                tenkinet_client_delete(context->tenkinetClient);
            }
            delete context;
            break;

        case Command::Code::CONNECT:
            if (context->state == Context::State::ZERO) {
                context->provider->onError();
            } else if (context->state == Context::State::DISCONNECTED) {
                context->state = Context::State::CONNECTING;
                ntp_query(context->ntpClient, NTP_INTERVAL_MS, NTP_TRIES);
            }
            break;

        case Command::Code::DISCONNECT:
            if (context->state == Context::State::CONNECTED) {
                tenkinet_disconnect(context->tenkinetClient);
            }
            break;

        case Command::Code::LIST:
            if (context->state == Context::State::CONNECTED) {
                tenkinet_req_list(context->tenkinetClient);
                tenkinet_send(context->tenkinetClient);
            }
            break;

        case Command::Code::SUBSCRIBE:
            if (context->state == Context::State::CONNECTED) {
                tenkinet_req_subscribe_all(context->tenkinetClient);
                tenkinet_req_data(context->tenkinetClient);
                tenkinet_req_poll(context->tenkinetClient, context->interval_ms * 1000);
                tenkinet_send(context->tenkinetClient);
            }
            break;

        case Command::Code::UNSUBSCRIBE:
            if (context->state == Context::State::CONNECTED) {
                tenkinet_req_unsubscribe_all(context->tenkinetClient);
                tenkinet_send(context->tenkinetClient);
            }
            break;

        case Command::Code::POLL:
            if (context->state == Context::State::CONNECTED) {
                tenkinet_req_poll(context->tenkinetClient, context->interval_ms * 1000);
                tenkinet_send(context->tenkinetClient);
            }
            break;
        }

        delete command;
    }

    commands.clear();
    return 0;
}

void TenkinetThread::shutdown() {

    mutex.lock();

    if (state != INIT) {
        state = SHUTDOWN;
    }

    mutex.unlock();
}

bool TenkinetThread::add(TenkinetProvider *provider) {

    QMutexLocker locker(&mutex);

    if (provider->handle) {
        return false;
    }

    Context *context = new Context();
    context->provider = provider;
    provider->handle = context;

    commands.append(new Command(Command::Code::ADD, context));

    return true;
}

bool TenkinetThread::remove(TenkinetProvider *provider) {

    QMutexLocker locker(&mutex);

    Context *context = (Context *)provider->handle;
    if (!context) {
        return false;
    }

    provider->handle = nullptr;

    commands.append(new Command(Command::Code::REMOVE, context));

    return true;
}

void TenkinetThread::connectClient(TenkinetProvider *provider) {

    QMutexLocker locker(&mutex);

    Context *context = (Context *)provider->handle;
    if (!context) {
        return;
    }

    commands.append(new Command(Command::Code::CONNECT, context));
}

void TenkinetThread::disconnectClient(TenkinetProvider *provider) {

    QMutexLocker locker(&mutex);

    Context *context = (Context *)provider->handle;
    if (!context) {
        return;
    }

    commands.append(new Command(Command::Code::DISCONNECT, context));
}

void TenkinetThread::refresh(TenkinetProvider *provider) {

    QMutexLocker locker(&mutex);

    Context *context = (Context *)provider->handle;
    if (!context) {
        return;
    }

    commands.append(new Command(Command::Code::LIST, context));
}

void TenkinetThread::setSubscribed(TenkinetProvider *provider, bool subscribed) {

    QMutexLocker locker(&mutex);

    Context *context = (Context *)provider->handle;
    if (!context) {
        return;
    }

    if (context->subscribed == subscribed) {
        return;
    }

    context->subscribed = subscribed;

    Command::Code code = subscribed ? Command::Code::SUBSCRIBE : Command::Code::UNSUBSCRIBE;
    commands.append(new Command(code, context));
}

void TenkinetThread::setPollInterval(TenkinetProvider *provider, unsigned int ms) {

    QMutexLocker locker(&mutex);

    Context *context = (Context *)provider->handle;
    if (!context) {
        return;
    }

    context->interval_ms = ms;

    commands.append(new Command(Command::Code::POLL, context));
}

void TenkinetThread::connectionCallback(TenkinetClient client, int status, void *user_data) {

    Context *context = (Context *)user_data;
    TenkinetProvider *provider = context->provider;

    if (status == TENKINET_SUCCESS) {

        tenkinet_req_align(client, context->origin);
        tenkinet_req_list(client);
        tenkinet_send(client);

        provider->setServerAddress(QString(tenkinet_get_server_address(client)));
        provider->setServerSerialNumber(QString(tenkinet_get_server_serial_number(client)));
        provider->setServerName(QString(tenkinet_get_server_name(client)));
        provider->setServerVersion(tenkinet_get_server_version(client));

        context->state = Context::State::CONNECTED;
        provider->onConnect();

    } else {
        context->state = Context::State::DISCONNECTED;
        provider->onDisconnect();
    }
}

void TenkinetThread::listCallback(TenkinetClient client, void *user_data) {

    Context *context = (Context *)user_data;
    TenkinetProvider *provider = context->provider;

    {
        QMutexLocker locker(&mutex);
        if (context->subscribed) {
            tenkinet_req_subscribe_all(client);
            tenkinet_req_data(client);
            tenkinet_req_poll(client, context->interval_ms * 1000);
            tenkinet_send(client);
        }
    }

    list_remove_all(&devices);
    tenkinet_get_devices(client, &devices);
    provider->onList(&devices);
}

void TenkinetThread::dataCallback(TenkinetClient client, int64_t timestamp, void *user_data) {

    Context *context = (Context *)user_data;
    TenkinetProvider *provider = context->provider;

    // Convert server time to local time
    timestamp -= context->ntpResult.offset_us;

    list_remove_all(&devices);
    size_t n = tenkinet_get_devices(client, &devices);

    provider->onData(timestamp, &devices);
}

void TenkinetThread::deviceStatusCallback(TenkinetClient client, int64_t timestamp, Device *device, void *user_data) {

    Context *context = (Context *)user_data;
    TenkinetProvider *provider = context->provider;

    // Convert server time to local time
    timestamp -= context->ntpResult.offset_us;

    if (device_flag_check(device, DEVICE_FLAG_ALIVE)) {
        provider->onDeviceConnect(timestamp, device);
    } else {
        provider->onDeviceDisconnect(timestamp, device);
    }
}

void TenkinetThread::ntpCallback(NTP_Client ntpClient, NTP_Result *result, void *user_data) {

    Context *context = (Context *)user_data;
    TenkinetProvider *provider = context->provider;

    // If NTP is unavailable, proceed without a clock delta
    if (result->status != NTP_CLIENT_SUCCESS) {
        result->delay_us = -1; // unsigned, so max value (infinite ping)
        result->offset_us = 0; // don't apply any offset
    }

    context->ntpResult = *result;
    context->origin = OriginTimestamp::instance().value() + result->offset_us;

    provider->onPing(result->delay_us);

    if (tenkinet_connect(context->tenkinetClient)) {
        context->state = Context::State::DISCONNECTED;
        provider->onError();
    }
}
