/****************************************************************************
 ** ScStw Libraries
 ** Copyright (C) 2020  Itsblue development
 **
 ** This program is free software: you can redistribute it and/or modify
 ** it under the terms of the GNU General Public License as published by
 ** the Free Software Foundation, either version 3 of the License, or
 ** (at your option) any later version.
 **
 ** This program is distributed in the hope that it will be useful,
 ** but WITHOUT ANY WARRANTY; without even the implied warranty of
 ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 ** GNU General Public License for more details.
 **
 ** You should have received a copy of the GNU General Public License
 ** along with this program.  If not, see <http://www.gnu.org/licenses/>.
 ****************************************************************************/

#include "scstwclient.h"

ScStwClient * pGlobalScStwClient = nullptr;

ScStwClient::ScStwClient(QObject * parent, QList<ScStw::SignalKey> signalSubscriptions) : QObject(parent)
{
    this->state = DISCONNECTED;
    this->currentRequestId = 1;
    this->extensions = QVariantMap({});
    this->signalSubscriptions = signalSubscriptions;

    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() {

    if(this->state != DISCONNECTED)
        return;

    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();

    QJsonArray signalSubs;
    foreach (ScStw::SignalKey key, this->signalSubscriptions) {
        signalSubs.append(key);
    }

    // init remote session
    QJsonObject sessionParams = {{"apiVersion", this->API_VERSION}, {"signalSubscriptions", signalSubs}, {"init", true}, {"usingTerminationKeys", true}};

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

    if(initResponse["status"] != 200) {
        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: " << this->firmwareVersion << " time offset: " << this->timeOffset;

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

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

    this->currentRequestId = 1;
    this->firmwareVersion = "";
    this->apiVersion = "";
    this->setExtensions(QVariantMap({}));

    for(WaitingRequest waitingRequest : this->waitingRequests.values())
        if(waitingRequest.loop != nullptr)
            waitingRequest.loop->exit(ScStw::NotConnectedError);

    this->waitingRequests.clear();

    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"}};
    }

    // pick a new request id that is not already active
    do {
        if(this->currentRequestId >= 99)
            this->currentRequestId = 0;
        else
            this->currentRequestId ++;
    } while(this->waitingRequests.contains(this->currentRequestId));

    int thisId = currentRequestId;
    //qDebug() << "sending command: " << header << " with data: " << data << " and id: " << thisId;

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

    this->waitingRequests.insert(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
    connect(timer, &QTimer::timeout, [=]{loop->exit(ScStw::TimeoutError);});
    // 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)
    ScStw::StatusCode statusCode = ScStw::StatusCode(loop->exec());

    // delete the timer
    timer->deleteLater();

    // find reply and delete the request from waiting list
    if(this->waitingRequests.contains(thisId)) {
        // request was found
        // delete event loop
        if(this->waitingRequests[thisId].loop != nullptr) {
            this->waitingRequests[thisId].loop->deleteLater();
        }

        // store reply
        reply = this->waitingRequests[thisId].reply;

        // remove reply from waiting list
        this->waitingRequests.remove(thisId);
    }
    else {
        // some internal error occured
        return {{"status", ScStw::InternalError}, {"data", ""}};
    }

    if(statusCode == ScStw::TimeoutError){
        //the time has been triggered -> timeout
        return {{"status", ScStw::TimeoutError}, {"data", ""}};
    }
    else if(statusCode == ScStw::NotConnectedError) {
        // connection was closed during request
        return {{"status", ScStw::NotConnectedError}, {"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: " << qPrintable(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!
        if(this->waitingRequests.contains(id)){
            this->waitingRequests[id].reply = replyObj;
            if(this->waitingRequests[id].loop != nullptr){
                this->waitingRequests[id].loop->exit(ScStw::Success);
            }
            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"].toMap());
        return;
        break;
    }
    default: {
        break;
    }
    }

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

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

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

QVariant ScStwClient::readRemoteSetting(ScStwSettings::BaseStationSetting key, ScStw::StatusCode* status) {
    QVariantMap reply = this->sendCommand(3001, int(key), 10000);
    qDebug() << "Setting read status is: " << reply["status"];

    if(status != nullptr)
        *status =  ScStw::StatusCode(reply["status"].toInt());

    if(reply["status"] != 200){
        return "false";
    }
    return reply["data"];
}

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();
    }
}

QVariantMap 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(QVariantMap extensions) {
    qDebug() << "[CLIENT][DEBUG] Extensions changed: " << extensions;

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


void ScStwClient::addSignalSubscription(ScStw::SignalKey key) {
    if(!this->signalSubscriptions.contains(key))
        this->signalSubscriptions.append(key);
}
