#include "../headers/scstwclient.h"

ScStwClient * pGlobalScStwClient = nullptr;

ScStwClient::ScStwClient(QObject * parent) : QObject(parent)
{
    this->state = DISCONNECTED;
    this->nextConnectionId = 1;
    this->extensions = QVariantList({});

    this->socket = new QTcpSocket(this);

    this->timeoutTimer = new QTimer(this);
    this->timeoutTimer->setSingleShot(true);

    connect(this->timeoutTimer, &QTimer::timeout,
            [=](){this->handleError(QAbstractSocket::ProxyConnectionTimeoutError);});

    connect(this->socket, SIGNAL(error(QAbstractSocket::SocketError)),
            this, SLOT(handleError(QAbstractSocket::SocketError)));

    connect(this->socket, &QAbstractSocket::stateChanged,
            this, &ScStwClient::handleSocketStateChange);

    connect(this->socket, &QAbstractSocket::readyRead,
            this, &ScStwClient::handleReadyRead);


    pGlobalScStwClient = this;
}

void ScStwClient::connectToHost() {

    setState(CONNECTING);

    //connect
    this->socket->connectToHost(this->ip, this->port);

    timeoutTimer->start(3000);
}

void ScStwClient::connectionTimeout() {
    if(this->state != CONNECTING)
        return;

    this->socket->abort();
    disconnect(this->timeoutTimer, SIGNAL(timeout()), this, SLOT(connectionTimeout()));
}

bool ScStwClient::init() {
    if(this->state != CONNECTING)
        return false;

    this->setState(INITIALISING);
    this->timeoutTimer->stop();

    // init remote session
    QJsonArray updateSubs = {ScStw::RaceStateChanged, ScStw::TimersChanged, ScStw::ExtensionsChanged, ScStw::NextStartActionChanged};
    QJsonObject sessionParams = {{"updateSubs", updateSubs}, {"init", true}, {"usingTerminationKeys", true}};

    QVariantMap initResponse = this->sendCommand(1, sessionParams, 3000, false);

    if(initResponse["status"] != 200) {
        this->closeConnection();
        return false;
    }

    this->apiVersion = initResponse["data"].toMap()["apiVersion"].toString();
    qDebug() << "[INFO][CLIENT] base station api version is: " << this->apiVersion;
    int compareResult = ScStw::firmwareCompare(this->API_VERSION, this->apiVersion);
    qDebug() << "compare result is: " << compareResult;
    if( compareResult == -3 ){
        // the client version is out of date!!
        this->closeConnection();
        return false;
    }
    else if(compareResult == 3){
        // the server version is out of date!!
        this->closeConnection();
        return false;
    }
    else if(compareResult == -4){
        // the server sent an invalid version
        this->closeConnection();
        return false;
    }

    this->firmwareVersion = initResponse["data"].toMap()["firmwareVersion"].toString();
    this->timeOffset = initResponse["data"].toMap()["time"].toDouble() - this->date->currentMSecsSinceEpoch();

    qDebug() << "[INFO][BaseStation] Init done! firmware: version: " << this->firmwareVersion << " up-to-date: " << this->isFirmwareUpToDate() << " time offset: " << this->timeOffset;

    this->setState(CONNECTED);
    return true;
}

void ScStwClient::deInit() {
    if(this->state == DISCONNECTED)
        return;

    this->setExtensions(QVariantList({}));
    this->setState(DISCONNECTED);
}

void ScStwClient::closeConnection()
{
    if(this->getState() == DISCONNECTED)
        return;

    qDebug() << "closing connection";
    switch (socket->state())
    {
    case 0:
        socket->disconnectFromHost();
        break;
    case 2:
        socket->abort();
        break;
    default:
        socket->abort();
    }
}

// -------------------------------------
// --- socket communication handling ---
// -------------------------------------

QVariantMap ScStwClient::sendCommand(int header, QJsonValue data, int timeout) {
    if(this->state != CONNECTED)
        return {{"status", ScStw::NotConnectedError}, {"data", "not connected"}};

    return this->sendCommand(header, data, timeout, true);
}

QVariantMap ScStwClient::sendCommand(int header, QJsonValue data, int timeout, bool useTerminationKeys) {
    if(this->state != CONNECTED && this->state != INITIALISING){
        return {{"status", ScStw::NotConnectedError}, {"data", "not connected"}};
    }

    // generate id and witing requests entry
    int thisId = nextConnectionId;
    //qDebug() << "sending command: " << header << " with data: " << data << " and id: " << thisId;
    nextConnectionId ++;

    QEventLoop *loop = new QEventLoop(this);
    QTimer *timer = new QTimer(this);
    QJsonObject reply;

    this->waitingRequests.append({thisId, loop, reply});

    QJsonObject requestObj;
    requestObj.insert("id", thisId);
    requestObj.insert("header", header);
    requestObj.insert("data", data);

    QString jsonRequest = QJsonDocument(requestObj).toJson();

    timer->setSingleShot(true);
    // quit the loop when the timer times out
    loop->connect(timer, SIGNAL(timeout()), loop, SLOT(quit()));
    // start the timer before starting to connect
    timer->start(timeout);

    //write data
    if(useTerminationKeys)
        socket->write(ScStw::SOCKET_MESSAGE_START_KEY + jsonRequest.toUtf8() + ScStw::SOCKET_MESSAGE_END_KEY);
    else
        socket->write(jsonRequest.toUtf8());

    //wait for an answer to finish (programm gets stuck in here)
    loop->exec();

    bool replyFound = false;

    // find reply and delete the request from waiting list
    for(int i = 0; i<this->waitingRequests.length(); i++){
        if(this->waitingRequests[i].id == thisId){
            // request was found
            replyFound = true;
            // delete event loop
            if(this->waitingRequests[i].loop != nullptr) {
                delete this->waitingRequests[i].loop;
            }
            // store reply
            reply = this->waitingRequests[i].reply;
            // remove reply from waiting list
            this->waitingRequests.removeAt(i);
        }
    }

    if(!replyFound) {
        // some internal error occured
        return {{"status", ScStw::Error}, {"data", ""}};
    }

    if(timer->remainingTime() == -1){
        //the time has been triggered -> timeout
        return {{"status", ScStw::TimeoutError}, {"data", ""}};
    }

    delete timer;
    return {{"status", reply.value("header").toInt()}, {"data", reply.value("data").toVariant()}};

}

void ScStwClient::handleSocketStateChange(QAbstractSocket::SocketState socketState) {
    switch (socketState) {
    case QAbstractSocket::UnconnectedState:
    {
        this->deInit();
        break;
    }
    case QAbstractSocket::ConnectedState:
    {
        if(!this->init()) {
            this->closeConnection();
        }

        break;
    }
    default:
    {
        //qDebug() << "+ --- UNKNOWN SOCKET STATE: " << socketState;
        break;
    }
    }
}

void ScStwClient::handleError(QAbstractSocket::SocketError err)
{
    if(err == QAbstractSocket::ProxyConnectionClosedError)
        this->closeConnection();

    switch (err) {
    case QAbstractSocket::ProxyConnectionTimeoutError:
        if(this->state == CONNECTING)
            this->closeConnection();
        break;
    default:
        break;
    }

    emit gotError(err);
    qDebug() << "got socket error: " << err;
}

void ScStwClient::handleReadyRead() {

    //qDebug() << "ready to ready " << socket->bytesAvailable() << " bytes" ;
    QString reply = socket->readAll();

    //qWarning() << "socket read: " << reply;

    processSocketMessage(reply);
}

void ScStwClient::processSocketMessage(QString message) {

    //qWarning() << "... processing message now ... : " << message;

    QString startKey = ScStw::SOCKET_MESSAGE_START_KEY;
    QString endKey = ScStw::SOCKET_MESSAGE_END_KEY;

    if(message == ""){
        return;
    }
    if((message.startsWith(startKey) && message.endsWith(endKey)) && (message.count(startKey) == 1 && message.count(endKey) == 1)){
        // non-split message ( e.g.: <message>123456789</message>
    }
    else if(!message.contains(endKey) && (!this->readBuffer.isEmpty() || message.startsWith(startKey))){
        // begin of a split message ( e.g.: <message>123 )
        // or middle of a split message ( e.g.: 456 )
        //qWarning() << "this is a begin or middle of split a message";
        this->readBuffer += message;
        return;
    }
    else if(!message.contains(startKey) && message.endsWith(endKey)) {
        // end of a split message ( e.g.: 789</message> )

        if(!this->readBuffer.isEmpty()){
            message = readBuffer + message;
            readBuffer.clear();
        }
    }
    else if((message.count(startKey) > 1 || message.count(endKey) > 1) || (message.contains(endKey) && !message.endsWith(endKey) && message.contains(startKey) && !message.startsWith(startKey))) {
        // multiple messages in one packet ( e.g.: <message>123456789</message><message>987654321</message> )
        // or multiple message fragments in one message ( e.g.: 56789</message><message>987654321</message> or 56789</message><message>98765 )
        //qDebug() << "detected multiple messages";

        int startOfSecondMessage = message.lastIndexOf(startKey);
        // process first part of message
        QString firstMessage = message.left(startOfSecondMessage);
        this->processSocketMessage(firstMessage);
        // process second part of message
        QString secondMessage = message.right(message.length() - startOfSecondMessage);
        this->processSocketMessage(secondMessage);

        return;
    }
    else {
        // invalid message
        return;
    }

    //qWarning() << "... done processing, message: " << message;
    this->handleSocketMessage(message);
}

void ScStwClient::handleSocketMessage(QString reply) {
    reply.replace(ScStw::SOCKET_MESSAGE_START_KEY, "");
    reply.replace(ScStw::SOCKET_MESSAGE_END_KEY, "");

    //qDebug() << "got message: " << reply;

    int id = 0;

    QJsonDocument jsonReply = QJsonDocument::fromJson(reply.toUtf8());
    QJsonObject replyObj = jsonReply.object();

    if(!replyObj.isEmpty()){
        id = replyObj.value("id").toInt();

        if(id == -1) {
            // this message is an update!!
            emit this->handleSignal(replyObj.toVariantMap());
            return;
        }

        // this message is the reply to a command!
        for(int i = 0; i < this->waitingRequests.length(); i++){
            if(this->waitingRequests[i].id == id){
                this->waitingRequests[i].reply = replyObj;
                if(this->waitingRequests[i].loop != nullptr){
                    this->waitingRequests[i].loop->quit();
                }
                return;
            }
        }
    }

    emit gotUnexpectedMessage(reply);
}

void ScStwClient::handleSignal(QVariantMap data) {
    // get the signal type
    if(ScStw::signalKeyFromInt(data["header"].toInt()) == ScStw::InvalidSignal)
        return;

    ScStw::SignalKey signalKey = ScStw::signalKeyFromInt(data["header"].toInt());

    //qDebug() << "got signal: " << signalKey << " with data: " << data["data"];

    switch (signalKey) {
    case ScStw::ExtensionsChanged:
    {
        // the extension connections have changed
        // -> handle locally
        this->setExtensions(data["data"].toList());
        return;
        break;
    }
    default: {
        break;
    }
    }

    // forward to external handlers
    emit this->gotSignal(signalKey, data["data"]);
}

// -------------------------
// --- updater functions ---
// -------------------------

bool ScStwClient::isFirmwareUpToDate() {
    QString file = ":/ScStwBasestation.sb64";
    QFile f(file);
    if (!f.open(QFile::ReadOnly)) return false;
    QString fileContents = f.readAll();

    QString newFirmwareVersion = fileContents.split("<VER>")[1].split("</VER>")[0];
    QString currentFirmwareVersion = this->firmwareVersion;

    return ScStw::firmwareCompare(newFirmwareVersion, currentFirmwareVersion) <= 0;
}

// ------------------------
// --- helper functions ---
// ------------------------

ScStw::StatusCode ScStwClient::writeRemoteSetting(ScStw::BaseStationSetting key, QString value) {
    QJsonArray requestData;
    requestData.append(key);
    requestData.append(value);
    return ScStw::StatusCode(this->sendCommand(3000, requestData)["status"].toInt());
}

QString ScStwClient::readRemoteSetting(ScStw::BaseStationSetting key) {
    QVariantMap reply = this->sendCommand(3001, key);
    if(reply["status"] != 200){
        return "false";
    }
    return reply["data"].toString();
}

void ScStwClient::setIP(QString newIp){
    this->ip = newIp;
}

QString ScStwClient::getIP()
{
    return this->ip;
}

ScStwClient::State ScStwClient::getState()
{
    return this->state;
}

void ScStwClient::setState(ScStwClient::State newState){
    if(this->state != newState) {
        qDebug() << "+--- ScStwClient state changed: " << newState;
        this->state = newState;
        emit stateChanged();
    }
}

QVariantList ScStwClient::getExtensions() {
    return this->extensions;
}

int ScStwClient::getTimeOffset() {
    return this->timeOffset;
}

QString ScStwClient::getFirmwareVersion() {
    return this->firmwareVersion;
}

QString ScStwClient::getApiVersion() {
    return this->apiVersion;
}

void ScStwClient::setExtensions(QVariantList extensions) {
    if(this->extensions != extensions){
        this->extensions = extensions;
        emit this->gotSignal(ScStw::ExtensionsChanged, this->getExtensions());
        emit this->extensionsChanged();
    }
}
