#include "leddisplaybackend.h"

LedDisplayBackend::LedDisplayBackend(QObject *parent) : QObject(parent)
{
    this->bleClient = new QBluetoothLeUartClient();
    this->bleClient->setUUIDs("92fecb20-1406-426a-afa5-cd5c1f306462", "92fecb21-1406-426a-afa5-cd5c1f306462", "92fecb22-1406-426a-afa5-cd5c1f306462");
    this->displayTextModel = new LedDisplayTextModel(this);
    this->textSetsBuffer.clear();
    this->displayBrightness = {{"displayBrightness", 0}, {"automaticBrightnessAdjustment", false}};
    this->runningCommands = 0;
    this->waitingCommands = 0;

    this->settings = new QSettings();

    this->keepAliveTimer = new QTimer(this);
    this->keepAliveTimer->setInterval(5000);
    this->keepAliveTimer->setSingleShot(false);
    connect(this->keepAliveTimer, &QTimer::timeout, this, &LedDisplayBackend::sendBluetoothKeepAlive);

    connect(this->bleClient, &QBluetoothLeUartClient::stateChanged, this, &LedDisplayBackend::handleBluetoothStateChange);
    connect(this->bleClient, &QBluetoothLeUartClient::foundNewDevice, this, &LedDisplayBackend::handleFoundNewDevice);
    connect(this->bleClient, &QBluetoothLeUartClient::dataReceived, this, &LedDisplayBackend::handleBluetoothDataReceived);
    connect(this->bleClient, &QBluetoothLeUartClient::connectedToDevice, this, &LedDisplayBackend::handleBluetoothDeviceConected);
    connect(this->displayTextModel, &LedDisplayTextModel::dataChanged, this, &LedDisplayBackend::handleDisplayTextModelDataChanged);
    connect(this->displayTextModel, &LedDisplayTextModel::rowsInserted, this, &LedDisplayBackend::handleDisplayTextModelRowsInserted);
    connect(this->displayTextModel, &LedDisplayTextModel::rowsRemoved, this, &LedDisplayBackend::handleDisplayTextModelRowsRemoved);

    this->setState(Idle);
    this->bleClient->startScanningForDevices();
}


void LedDisplayBackend::startScanning() {
    this->bleClient->startScanningForDevices();
}

void LedDisplayBackend::authenticate(QString code) {
    // tell display to send over existing model data
    this->setState(Authenticating);

    if(code.length() == 64)
        this->lastDisplaySecret = code;
    else if(code.length() == 4) {
        QString combinedCode = code;
        this->lastDisplaySecret = QCryptographicHash::hash(combinedCode.toUtf8(), QCryptographicHash::Sha256).toHex();
    }

    this->sendBluetoothCommand(AuthenticateCommand, QVariantMap{{"secret", this->lastDisplaySecret}});
}

void LedDisplayBackend::handleBluetoothScanningError(QBluetoothLeUartClient::BluetoothScanError error) {
    Q_UNUSED(error)
}

void LedDisplayBackend::handleBluetoothStateChange(QBluetoothLeUartClient::BluetoothLeUartClientState state){
    switch(state){
    case QBluetoothLeUartClient::Idle: {
        this->setState(Idle);
        break;
    }
    case QBluetoothLeUartClient::AdapterTurnedOff: {
        this->setState(BluetoothOff);
        break;
    }
    case QBluetoothLeUartClient::LocationPermissionDenied: {
        this->setState(LocationPermissionDenied);
        break;
    }
    case QBluetoothLeUartClient::Scanning: {
        this->setState(Scanning);
        break;
    }
    case QBluetoothLeUartClient::ScanFinished: {
        this->setState(ReadyToConnect);
        break;
    }
    case QBluetoothLeUartClient::Connecting: {
        this->setState(Connecting);
        break;
    }
    case QBluetoothLeUartClient::ScanningForService: {
        this->setState(Connecting);
        break;
    }
    case QBluetoothLeUartClient::ServiceFound: {
        this->setState(Connecting);
        break;
    }
    case QBluetoothLeUartClient::Connected:
    {
        break;
    }
    }

    if(state == QBluetoothLeUartClient::Connected)
        this->keepAliveTimer->start();
    else if(this->keepAliveTimer->isActive())
        this->keepAliveTimer->stop();
}

void LedDisplayBackend::handleBluetoothDeviceConected() {
    if(this->settings->contains(this->bleClient->getCurrentDevice()->getAddress()))
        this->authenticate(this->settings->value(this->bleClient->getCurrentDevice()->getAddress()).toString());
    else
        this->setState(AuthenticationRequired);
}

void LedDisplayBackend::handleFoundNewDevice(QBluetoothLeUartDevice* device) {
    qDebug() << "Found a device: name: " << device->getName() << " address: " << device->getAddress();
}

void LedDisplayBackend::handleDisplayTextModelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles) {

    if(this->state == Initing) return;

    qDebug() << "Data changed: topLeft: " << topLeft << " bottomRight: " << bottomRight << " roles: " << roles;

    for(int role : roles)
        this->updateDisplayTextSetParameter(topLeft.row(), role);
}

void LedDisplayBackend::handleDisplayTextModelRowsInserted(const QModelIndex &parent, int first, int last) {
    qDebug() << "Rows inserted: parent: " << parent << " first: " << first << " last " << last;
    for(int i = 0; i < LedDisplayTextModel::LedDisplayTextModelRoleCount; i++) {
        this->updateDisplayTextSetParameter(first, i);
    }
}

void LedDisplayBackend::handleDisplayTextModelRowsRemoved(const QModelIndex &parent, int first, int last) {
    qDebug() << "Rows removed: parent: " << parent << " first: " << first << " last " << last;
    // Setting Text to "" will delete the item
    this->updateDisplayTextSetParameter(first, LedDisplayTextModel::TextRole, "");
}


void LedDisplayBackend::sendBluetoothCommand(OmobiDisplayCommand command, QVariant data) {
    QVariantMap commandMap = {
        {"header", command},
        {"data", data}
    };

    QJsonDocument doc = QJsonDocument::fromVariant(commandMap);

    qDebug() << "Sending command: \n" << qPrintable(doc.toJson(QJsonDocument::Compact));

    if((this->state != Initing && this->runningCommands > 0) || (this->state == Initing && this->runningCommands > 1)) {
        this->waitingCommands++;
        QEventLoop loop;
        loop.connect(this, &LedDisplayBackend::commandFinished, &loop, &QEventLoop::quit);
        loop.exec();
        this->waitingCommands--;
        if(this->state == Idle) return;
    }

    this->runningCommands ++;

    if(this->state == Connected)
        this->setState(Loading);

    this->bleClient->sendData(doc.toJson(QJsonDocument::Compact));
}

void LedDisplayBackend::sendBluetoothKeepAlive() {
    QJsonDocument doc = QJsonDocument::fromVariant(QVariantMap{{"header", KeepAliveCommand}});

    //qDebug() << "Sending keep alive: \n" << qPrintable(doc.toJson(QJsonDocument::Indented));

    this->bleClient->sendData(doc.toJson(QJsonDocument::Compact));
}

void LedDisplayBackend::handleBluetoothDataReceived(QString s){
    qDebug() << "New data: \n" << qPrintable(s);

    QJsonParseError parseError;
    QJsonDocument doc = QJsonDocument::fromJson(s.toUtf8(), &parseError);

    if(parseError.error != QJsonParseError::NoError)
        return;

    OmobiDisplayCommand header = OmobiDisplayCommand(doc.toVariant().toMap()["header"].toInt());
    QVariantMap data = doc.toVariant().toMap()["data"].toMap();
    OmobiDisplayStatusCode status = OmobiDisplayStatusCode(doc.toVariant().toMap()["status"].toInt());

    qDebug() << "Header is:" << header;

    switch (header) {
    case AuthenticateCommand: {
        if(status != Success) {
            this->setState(AuthenticationRequired);
            this->lastDisplaySecret = "";
            return;
        }

        this->settings->setValue(this->bleClient->getCurrentDevice()->getAddress(), this->lastDisplaySecret);

        this->runningCommands = 0;
        emit this->commandFinished();
        this->setLoadingProgress(0);
        this->setState(Initing);
        this->sendBluetoothCommand(GetAllTextSetsCommand);

        break;
    }
    case KeepAliveCommand: {
        break;
    }

    case GetAllTextSetsCommand: {
        // indicates that all existing text sets have been sent over after GetAllTextSetsCommand was called
        if(status != Success)
            // TODO: handle error
            break;

        this->displayTextModel->maximumTextLength = data["maximumTextLength"].toInt();
        this->displayTextModel->maximumTextSets = data["maximumTextSets"].toInt();

        this->displayTextModel->setTexts({});
        this->textSetsBuffer.clear();

        for(int i = 0; i < this->displayTextModel->maximumTextSets; i++) {
            for(int param = 0; param < DisplayTextSetParameterCount; param++) {
                this->setLoadingProgress(float((i*DisplayTextSetParameterCount) + param +1) / float(this->displayTextModel->maximumTextSets * DisplayTextSetParameterCount));
                this->sendBluetoothCommand(GetTextSetParameterCommand, QVariantMap({{"index", i}, {"parameter", param}}));
            }
        }
        this->sendBluetoothCommand(GetDisplayBrightnessCommand);

        this->refreshLoadingState();

        break;
    }
    case GetTextSetParameterCommand: {

        int index = data["index"].toInt();

        if(index < 0)
            return;

        while(this->textSetsBuffer.length() <= index)
            this->textSetsBuffer.append(QMap<int, QVariant>());

        int parameter = data["parameter"].toInt();

        if(!this->textSetsBuffer[index].contains(parameter))
            this->textSetsBuffer[index].insert(parameter, QVariant());

        this->textSetsBuffer[index][parameter] = data["value"].toString();
        qDebug() << "Got value: " << this->textSetsBuffer[index][parameter];
        this->displayTextModel->setTexts(this->textSetsBuffer);
        this->refreshLoadingState();

        break;
    }
    case GetDisplayBrightnessCommand: {
        this->setDisplayBrightness(data);
        this->refreshLoadingState();
        break;
    }
    case SetTextSetParameterCommand:
    case SetDisplayBrightnessCommand:
    case SetDisplayCodeCommand:
    case SetDisplayNameCommand:
        // TODO: Error handling
        this->refreshLoadingState();
        break;


    }
}


void LedDisplayBackend::updateDisplayTextSetParameter(int index, int parameter) {
    this->updateDisplayTextSetParameter(index, parameter, this->displayTextModel->data(index, parameter).toString());
}

void LedDisplayBackend::updateDisplayTextSetParameter(int index, int parameter, QString value) {
    if(this->state == Initing)
        return;
    qDebug() << "Updating data at index: " << index << " parameter: " << parameter << " and value: " << value;

    QVariantMap dataMap = {
        {"index", index},
        {"parameter", parameter},
        {"value", value}
    };

    this->sendBluetoothCommand(SetTextSetParameterCommand, dataMap);
}

QBluetoothLeUartClient* LedDisplayBackend::getBleClient() {
    return this->bleClient;
}


LedDisplayTextModel* LedDisplayBackend::getDisplayTextModel() {
    return this->displayTextModel;
}

LedDisplayBackend::OmobiDisplayAppState LedDisplayBackend::getState() {
    return this->state;
}

void LedDisplayBackend::refreshLoadingState() {
    if(this->state != Initing && this->state != Loading)
        return;

    qDebug() << "Refreshing loading state! Waiting: " << this->runningCommands;
    emit this->commandFinished();

    if(this->runningCommands <= 1 && this->waitingCommands == 0) {
        this->runningCommands = 0;
        this->setState(Connected);
        this->setLoadingProgress(0);
    }
    else
        this->runningCommands--;
}

void LedDisplayBackend::setState(OmobiDisplayAppState state) {
    if(state == this->state)
        return;

    this->state = state;
    emit this->stateChanged();

    qDebug() << "Now in " << state << " state";

    if(this->state == Idle) {
        this->runningCommands = 0;
        emit this->commandFinished();
        this->bleClient->startScanningForDevices();
    }
}

QVariantMap LedDisplayBackend::getDisplayBrightness() {
    return this->displayBrightness;
}

void LedDisplayBackend::setDisplayBrightness(QVariantMap brightness) {
    if(brightness == this->displayBrightness)
        return;

    this->displayBrightness = brightness;

    this->sendBluetoothCommand(SetDisplayBrightnessCommand, this->displayBrightness);

    emit this->displayBrightnessChanged();
}


void LedDisplayBackend::setDisplayCode(QString code) {
    this->sendBluetoothCommand(SetDisplayCodeCommand, QVariantMap{{"displayCode",code}});
}

void LedDisplayBackend::setDisplayName(QString name) {
    // This will restart the display!!
    this->sendBluetoothCommand(SetDisplayNameCommand, QVariantMap{{"displayName", name}});
}

void LedDisplayBackend::setLoadingProgress(float progress) {
    this->loadingProgress = progress;
    emit this->loadingProgressChanged();
}

float LedDisplayBackend::getLoadingProgress() {
    return this->loadingProgress;
}
