#include <QApplication>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QThread>

#include <iostream>
#include <mutex>
#include <string>

#include "rkt_protocol.h"
#include "rktwebview_qt.h"
#include "utils.h"

namespace {

constexpr int STDIO_PROTOCOL_VERSION = 1;

std::mutex output_mutex;

void writeMessage(const QJsonObject &message)
{
    const QByteArray json = QJsonDocument(message).toJson(QJsonDocument::Compact);
    std::lock_guard<std::mutex> lock(output_mutex);
    std::cout.write(json.constData(), json.size());
    std::cout.put('\n');
    std::cout.flush();
}

void sendReady()
{
    QJsonObject message;
    message["type"] = "ready";
    message["protocol"] = STDIO_PROTOCOL_VERSION;
    writeMessage(message);
}

void sendResult(int id,
                int result,
                const QJsonValue &data = QJsonValue(QJsonValue::Null))
{
    QJsonObject message;
    message["type"] = "result";
    message["id"] = id;
    message["result"] = result;
    message["data"] = data;
    writeMessage(message);
}

void sendProtocolError(const QString &messageText, int id = -1)
{
    QJsonObject message;
    message["type"] = "protocol-error";
    if (id >= 0) {
        message["id"] = id;
    }
    message["message"] = messageText;
    writeMessage(message);
}

void freeData(rkt_data_t *data)
{
    do_free_data(data);
}

class Handler : public QThread
{
public:
    Rktwebview_qt *webviewHandler = nullptr;

protected:
    void run() override;
};


void eventCallback(rkt_data_t *data)
{
    if (data == nullptr || data->kind != rkt_data_kind_t::event) {
        freeData(data);
        return;
    }

    QJsonObject message;
    message["type"] = "event";
    message["wv"] = data->data.event.wv;
    message["data"] = QString::fromUtf8(data->data.event.event);
    writeMessage(message);

    freeData(data);
}

} // namespace

int main(int argc, char *argv[])
{
    INFO0("Starting stdio backend\n");

    Handler commandHandler;
    commandHandler.webviewHandler = new Rktwebview_qt(argc, argv);
    commandHandler.webviewHandler->initApp();
    commandHandler.start();

    sendReady();
    commandHandler.webviewHandler->execApp();

    INFO0("Qt event loop stopped; waiting for stdin handler\n");
    commandHandler.wait();

    delete commandHandler.webviewHandler;
    commandHandler.webviewHandler = nullptr;

    INFO0("Exiting stdio backend\n");
    return 0;
}

void Handler::run()
{
    std::string line;
    bool quit = false;

    while (!quit && std::getline(std::cin, line)) {
        if (line.empty()) {
            continue;
        }

        QJsonParseError parseError;
        const QJsonDocument document =
            QJsonDocument::fromJson(QByteArray::fromStdString(line), &parseError);

        if (parseError.error != QJsonParseError::NoError || !document.isObject()) {
            const QString message = QString("Invalid protocol JSON: %1")
                                        .arg(parseError.errorString());
            ERROR1("%s\n", message.toUtf8().constData());
            sendProtocolError(message);
            continue;
        }

        const QJsonObject request = document.object();
        const int id = request["id"].toInt(-1);

        if (request["type"].toString() != "command") {
            sendProtocolError("Expected a command message", id);
            continue;
        }

        const int command = request["command"].toInt(CMD_NOOP);
        const QJsonObject data = request["data"].toObject();

        if (id < 0) {
            sendProtocolError("Command has no valid request id");
            continue;
        }

        switch (command) {
        case CMD_NOOP:
            sendResult(id, result_t::oke);
            break;

        case CMD_QUIT:
            INFO0("Got quit command\n");
            webviewHandler->rktQuit();
            sendResult(id, RESULT_QUIT);
            quit = true;
            break;

        case CMD_INFO:
            sendResult(id, webviewHandler->openWindows());
            break;

        case CMD_HANDLE_IS_VALID: {
            const int wv = data["wv"].toInt();
            sendResult(id, webviewHandler->rktValid(wv) ? 1 : 0);
            break;
        }

        case CMD_SET_LOGLEVEL: {
            const int level = data["wv"].toInt();
            setLogLevel(level);
            sendResult(id, result_t::oke);
            break;
        }

        case CMD_CONTEXT_NEW: {
            const QString boilerplateJs = data["boilerplate_js"].toString();
            const bool hasPemCertificate = data["has_pem_cert"].toBool();
            const QString pemCertificate = data["pem_cert"].toString();
            const int context = webviewHandler->newContext(boilerplateJs,
                                                           hasPemCertificate,
                                                           pemCertificate);
            sendResult(id, context);
            break;
        }

        case CMD_CREATE_WV: {
            const int context = data["context"].toInt();
            const int parent = data["parent"].toInt();
            const int wv = webviewHandler->rktWebViewCreate(context,
                                                            parent,
                                                            eventCallback);
            sendResult(id, wv);
            break;
        }

        case CMD_CLOSE_WV: {
            const int wv = data["wv"].toInt();
            webviewHandler->rktWebViewClose(wv);
            sendResult(id, result_t::oke);
            break;
        }

        case CMD_SET_URL: {
            const int wv = data["wv"].toInt();
            const QString url = data["url"].toString();
            sendResult(id, webviewHandler->rktSetUrl(wv, url.toUtf8().constData()));
            break;
        }

        case CMD_SET_HTML: {
            const int wv = data["wv"].toInt();
            const QString html = data["html"].toString();
            sendResult(id, webviewHandler->rktSetHtml(wv, html.toUtf8().constData()));
            break;
        }

        case CMD_RUN_JS: {
            const int wv = data["wv"].toInt();
            const QString js = data["js"].toString();
            sendResult(id, webviewHandler->rktRunJs(wv, js));
            break;
        }

        case CMD_CALL_JS: {
            const int wv = data["wv"].toInt();
            const QString js = data["js"].toString();
            rkt_data_t *response = webviewHandler->rktCallJs(wv, js.toUtf8().constData());
            if (response == nullptr || response->kind != rkt_data_kind_t::js_result) {
                sendResult(id, result_t::failed, "Missing JavaScript result");
            } else {
                sendResult(id,
                           response->data.js_result.result,
                           QString::fromUtf8(response->data.js_result.value));
            }
            freeData(response);
            break;
        }

        case CMD_OPEN_DEVTOOLS: {
            const int wv = data["wv"].toInt();
            sendResult(id, webviewHandler->rktOpenDevtools(wv));
            break;
        }

        case CMD_MOVE: {
            const int wv = data["wv"].toInt();
            sendResult(id,
                       webviewHandler->rktMove(wv,
                                               data["x"].toInt(),
                                               data["y"].toInt()));
            break;
        }

        case CMD_RESIZE: {
            const int wv = data["wv"].toInt();
            sendResult(id,
                       webviewHandler->rktResize(wv,
                                                 data["w"].toInt(),
                                                 data["h"].toInt()));
            break;
        }

        case CMD_HIDE: {
            const int wv = data["wv"].toInt();
            sendResult(id, webviewHandler->rktHideWindow(wv));
            break;
        }

        case CMD_SHOW: {
            const int wv = data["wv"].toInt();
            sendResult(id, webviewHandler->rktShowWindow(wv));
            break;
        }

        case CMD_PRESENT: {
            const int wv = data["wv"].toInt();
            sendResult(id, webviewHandler->rktPresentWindow(wv));
            break;
        }

        case CMD_MAXIMIZE: {
            const int wv = data["wv"].toInt();
            sendResult(id, webviewHandler->rktMaximizeWindow(wv));
            break;
        }

        case CMD_MINIMIZE: {
            const int wv = data["wv"].toInt();
            sendResult(id, webviewHandler->rktMinimizeWindow(wv));
            break;
        }

        case CMD_SHOW_NORMAL: {
            const int wv = data["wv"].toInt();
            sendResult(id, webviewHandler->rktShowNormalWindow(wv));
            break;
        }

        case CMD_WINDOW_STATE: {
            const int wv = data["wv"].toInt();
            sendResult(id, webviewHandler->rktWindowState(wv));
            break;
        }

        case CMD_SET_TITLE: {
            const int wv = data["wv"].toInt();
            const QString title = data["title"].toString();
            sendResult(id,
                       webviewHandler->rktWindowSetTitle(wv,
                                                         title.toUtf8().constData()));
            break;
        }

        case CMD_SET_ICON: {
            const int wv = data["wv"].toInt();
            const QString icon = data["icon"].toString();
            sendResult(id,
                       webviewHandler->rktWindowSetIcon(wv,
                                                        icon.toUtf8().constData()));
            break;
        }

        case CMD_CREATE_TRAY: {
            const QString icon = data["icon"].toString();
            const QString tooltip = data["tooltip"].toString();
            const int tray = webviewHandler->rktTrayCreate(icon.toUtf8().constData(),
                                                           tooltip.toUtf8().constData(),
                                                           eventCallback);
            sendResult(id, tray);
            break;
        }

        case CMD_TRAY_SET_ICON: {
            const int tray = data["wv"].toInt();
            const QString icon = data["icon"].toString();
            sendResult(id,
                       webviewHandler->rktTraySetIcon(tray,
                                                      icon.toUtf8().constData()));
            break;
        }

        case CMD_TRAY_SET_TOOLTIP: {
            const int tray = data["wv"].toInt();
            const QString tooltip = data["tooltip"].toString();
            sendResult(id,
                       webviewHandler->rktTraySetTooltip(tray,
                                                         tooltip.toUtf8().constData()));
            break;
        }

        case CMD_TRAY_SHOW_MESSAGE: {
            const int tray = data["wv"].toInt();
            const QString title = data["title"].toString();
            const QString message = data["message"].toString();
            sendResult(id,
                       webviewHandler->rktTrayShowMessage(tray,
                                                          title.toUtf8().constData(),
                                                          message.toUtf8().constData()));
            break;
        }

        case CMD_TRAY_CLEAR_MENU: {
            const int tray = data["wv"].toInt();
            sendResult(id, webviewHandler->rktTraySetMenu(tray, nullptr));
            break;
        }

        case CMD_TRAY_SET_MENU: {
            const int tray = data["wv"].toInt();
            const QString menuJson = data["menu_json"].toString();
            sendResult(id,
                       webviewHandler->rktTraySetMenu(tray,
                                                      menuJson.toUtf8().constData()));
            break;
        }

        case CMD_CHOOSE_DIR: {
            const int wv = data["wv"].toInt();
            const QString title = data["title"].toString();
            const QString baseDir = data["base_dir"].toString();
            sendResult(id,
                       webviewHandler->rktChooseDir(wv,
                                                    title.toUtf8().constData(),
                                                    baseDir.toUtf8().constData()));
            break;
        }

        case CMD_FILE_OPEN: {
            const int wv = data["wv"].toInt();
            const QString title = data["title"].toString();
            const QString baseDir = data["base_dir"].toString();
            const QString permittedExtensions = data["permitted_exts"].toString();
            sendResult(id,
                       webviewHandler->rktFileOpen(wv,
                                                   title.toUtf8().constData(),
                                                   baseDir.toUtf8().constData(),
                                                   permittedExtensions.toUtf8().constData()));
            break;
        }

        case CMD_FILE_SAVE: {
            const int wv = data["wv"].toInt();
            const QString title = data["title"].toString();
            const QString baseDir = data["base_dir"].toString();
            const QString permittedExtensions = data["permitted_exts"].toString();
            sendResult(id,
                       webviewHandler->rktFileSave(wv,
                                                   title.toUtf8().constData(),
                                                   baseDir.toUtf8().constData(),
                                                   permittedExtensions.toUtf8().constData()));
            break;
        }

        case CMD_SET_OU_TOKEN: {
            const int wv = data["wv"].toInt();
            const QString token = data["token"].toString();
            webviewHandler->rktSetOUToken(wv, token.toUtf8().constData());
            sendResult(id, result_t::oke);
            break;
        }

        case CMD_MSG_BOX: {
            const int wv = data["wv"].toInt();
            const QString title = data["title"].toString();
            const QString message = data["message"].toString();
            const QString submessage = data["submessage"].toString();
            const auto type = static_cast<rkt_messagetype_t>(data["type"].toInt());
            sendResult(id,
                       webviewHandler->rktMessageBox(wv,
                                                     title.toUtf8().constData(),
                                                     message.toUtf8().constData(),
                                                     submessage.toUtf8().constData(),
                                                     type));
            break;
        }

        default: {
            const QString message = QString("Unknown command: %1").arg(command);
            ERROR1("%s\n", message.toUtf8().constData());
            sendResult(id, result_t::failed, message);
            break;
        }
        }
    }

    if (!quit) {
        INFO0("stdin closed; stopping Qt backend\n");
        webviewHandler->closeAllWindows();
        webviewHandler->rktQuit();
    }

    DEBUG0("Exiting stdin handler\n");
}
