/*
 * Copyright 2024 KylinSoft Co., Ltd.
 *
 * 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 <https://www.gnu.org/licenses/>.
 */

#include "corevisionserviceproxy.h"

#include <openssl/bio.h>
#include <openssl/buffer.h>
#include <openssl/evp.h>

#include <string>
#include <thread>
#include <vector>

#include "_textrecognitionconfig.h"
#include "_textrecognitionresult.h"
#include "logger.h"
#include "util.h"

namespace kyai {
namespace core {
namespace vision {

const std::string serverUnixPath = "unix:path=/tmp/.kylin-ai-runtime-unix/" +
                                   std::to_string(getuid()) +
                                   "/core-vision.sock";
const char *objectPath = "/com/kylin/AiRuntime/CoreVision";

void dissectRecognitionResult(GVariant *parameters,
                              TextRecognitionResult *recognitionResult) {
    if (parameters == nullptr || recognitionResult == nullptr) {
        return;
    }
    GVariantIter iter;
    g_variant_iter_init(&iter, parameters);

    GVariant *text = g_variant_iter_next_value(&iter);
    GVariant *lines = g_variant_iter_next_value(&iter);
    GVariant *errorCode = g_variant_iter_next_value(&iter);
    GVariant *errorMessage = g_variant_iter_next_value(&iter);

    if (!text || !lines || !errorCode || !errorMessage) {
        LOGE("recognition result error: result is nullptr!");
        return;
    }

    const gchar *resultText = g_variant_get_string(text, nullptr);
    recognitionResult->allText = std::string(resultText);
    g_variant_unref(text);

    GVariantIter linesIter;
    g_variant_iter_init(&linesIter, lines);

    while (GVariant *lineVariant = g_variant_iter_next_value(&linesIter)) {
        TextLine *line = new TextLine{};
        GVariant *pointsValue;
        double conf;
        gchar *oneTextLine;

        g_variant_get(lineVariant, "(s@a(ii)d)", &oneTextLine, &pointsValue,
                      &conf);
        if (!oneTextLine || !pointsValue) {
            LOGE("recognition result error: result is nullptr!");
            g_variant_unref(lineVariant);
            g_variant_unref(lines);
            delete line;
            return;
        }

        GVariantIter pointsIter;
        g_variant_iter_init(&pointsIter, pointsValue);

        while (GVariant *pointVariant =
                   g_variant_iter_next_value(&pointsIter)) {
            PixelPoint point;
            g_variant_get(pointVariant, "(ii)", &point.x, &point.y);
            if (point.x < 0 || point.y < 0) {
                LOGE("recognition result point error: pixel point < 0!");
                point.x = 0;
                point.y = 0;
            }
            line->points.push_back(point);
            g_variant_unref(pointVariant);
        }
        g_variant_unref(pointsValue);
        line->text = oneTextLine;
        recognitionResult->lines.push_back(line);
        g_free(oneTextLine);
        g_variant_unref(lineVariant);
    }
    g_variant_unref(lines);

    int resultErrorCode = g_variant_get_int32(errorCode);
    recognitionResult->errorCode = resultErrorCode;
    g_variant_unref(errorCode);

    const gchar *resultErrorMessage =
        g_variant_get_string(errorMessage, nullptr);
    recognitionResult->errorMessage = std::string(resultErrorMessage);
    g_variant_unref(errorMessage);
}

CoreVisionServer &CoreVisionServer::getInstance() {
    static CoreVisionServer instance;
    return instance;
}

CoreVisionServer::~CoreVisionServer() { destroy(); }

void CoreVisionServer::destroy() {
    if (connection_ != nullptr) {
        g_object_unref(connection_);
    }
}

void CoreVisionServer::init() { init(serverUnixPath); }

void CoreVisionServer::init(const std::string &unixPath) {
    GError *error = nullptr;
    connection_ = g_dbus_connection_new_for_address_sync(
        unixPath.c_str(), G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT,
        nullptr, nullptr, &error);
    if (connection_ == nullptr) {
        g_printerr("Error connecting to D-Bus address %s: %s\n",
                   unixPath.c_str(), error->message);
        g_error_free(error);
    }
}

void CoreVisionServiceProxy::setRecognitionResultCallback(
    TextRecognitionResultCallback callback, void *userData) {
    recognitionCallback_ = callback;
    recognitionUserData_ = userData;
}

void CoreVisionServiceProxy::setReconnectedCallback(
    ReconnectedCallback callback) {
    reconnectedCallback_ = std::move(callback);
}

CoreVisionServiceProxy::~CoreVisionServiceProxy() {
    if (proxy_) {
        g_object_unref(proxy_);
    }
}

bool CoreVisionServiceProxy::connectToServer() {
    CoreVisionServer::getInstance().init();
    if (!CoreVisionServer::getInstance().available()) {
        g_printerr(
            "Error creating core vision server proxy: Server proxy "
            "connection is unavailable.\n");
        return false;
    }

    if (proxy_ != nullptr) {
        g_object_unref(proxy_);
        proxy_ = nullptr;
    }

    GError *error = nullptr;
    auto *connection = CoreVisionServer::getInstance().getConnection();
    proxy_ = ai_runtime_core_vision_service_proxy_new_sync(
        connection, G_DBUS_PROXY_FLAGS_NONE, nullptr, objectPath, nullptr,
        &error);
    if (proxy_ == nullptr) {
        LOGE("Error creating core vision service proxy: %s\n", error->message);
        g_error_free(error);
        return false;
    }

    g_dbus_proxy_set_default_timeout(G_DBUS_PROXY(proxy_), 5 * 60 * 1000);

    return true;
}

int CoreVisionServiceProxy::initEngine(const char *config, int *sessionId) {
    int errorCode = AISDK_NO_ERROR;
    GError *error = nullptr;
    bool success = ai_runtime_core_vision_service_call_init_sync(
        proxy_, config, sessionId, &errorCode, nullptr, &error);
    if (not success) {
        if (error) {
            LOGE("Error calling init: {}:{}", error->code, error->message);
            g_error_free(error);
            return AISDK_RUNTIME_ERROR;
        }
    }

    return errorCode;
}

void CoreVisionServiceProxy::destroyTextRecognition(int sessionId) {
    ai_runtime_core_vision_service_call_destroy_sync(proxy_, sessionId, nullptr,
                                                     nullptr);
}

void CoreVisionServiceProxy::recognizeTextFromImageDataAsync(
    int sessionId, const char *data, unsigned int imageDataLength) {
    if (proxy_ == nullptr) {
        LOGE("Proxy not exist!");
        return;
    }
    std::string imageData(data, imageDataLength);
    std::string base64ImageData = util::base64Encode(imageData);
    ai_runtime_core_vision_service_call_recognize_text_from_image_data(
        proxy_, sessionId, base64ImageData.c_str(), base64ImageData.size(),
        nullptr, onRecognitionResultCallback, this);
}

void CoreVisionServiceProxy::recognizeTextFromImageFileAsync(
    int sessionId, const char *imageFile) {
    if (proxy_ == nullptr) {
        LOGE("Proxy not exist!");
        return;
    }
    ai_runtime_core_vision_service_call_recognize_text_from_image_file(
        proxy_, sessionId, imageFile, nullptr, onRecognitionResultCallback,
        this);
}

void CoreVisionServiceProxy::onRecognitionResultCallback(GObject *sourceObject,
                                                         GAsyncResult *res,
                                                         gpointer userData) {
    GVariant *result = nullptr;
    GError *error = nullptr;
    auto *proxy = static_cast<CoreVisionServiceProxy *>(userData);
    bool ret =
        ai_runtime_core_vision_service_call_recognize_text_from_image_file_finish(
            (AiRuntimeCoreVisionService *)sourceObject, &result, res, &error);
    if (!ret) {
        if (error) {
            LOGE("Error calling recognizeTextFromImageFile: {}:{}", error->code,
                 error->message);
            proxy->handleErrorOccurred(error->code);
            g_error_free(error);
        }
        if (result) {
            g_variant_unref(result);
        }
        return;
    }

    if (proxy->recognitionCallback_ != nullptr) {
        TextRecognitionResult textRecognitionResult = {};
        dissectRecognitionResult(result, &textRecognitionResult);
        g_variant_unref(result);
        proxy->recognitionCallback_(&textRecognitionResult,
                                    proxy->recognitionUserData_);
    } else {
        LOGE("Recognizing callback is not set!");
    }
}

void CoreVisionServiceProxy::handleErrorOccurred(int errorCode) {
    LOGE("Server disconnected.");
    switch (errorCode) {
        case G_IO_ERROR_CLOSED: {
            handleServerClosed();
            break;
        }
        case G_IO_ERROR_TIMED_OUT: {
            handleServerTimeout();
            break;
        }
    }
}

void CoreVisionServiceProxy::handleServerClosed() {
    LOGE("Server closed unexpectedly.");
    if (recognitionCallback_) {
        TextRecognitionResult result{
            .errorCode = AISDK_RUNTIME_ERROR,
            .errorMessage = "Server closed unexpectedly."};
        recognitionCallback_(&result, recognitionUserData_);
    }

    if (reconnectServer()) {
        if (reconnectedCallback_) {
            std::fprintf(stderr, "Reconnected to server And call back.\n");
            reconnectedCallback_();
        }
    }

    reconnectAttempts_ = 0;
}

void CoreVisionServiceProxy::handleServerTimeout() {
    if (recognitionCallback_) {
        TextRecognitionResult result{.errorCode = AISDK_SERVICE_TIMEOUT,
                                     .errorMessage = "Server timeout."};
        recognitionCallback_(&result, recognitionUserData_);
    }
}

bool CoreVisionServiceProxy::reconnectServer() {
    if (reconnectAttempts_ >= maxReconnectAttempts) {
        LOGE("Max reconnect attempts reached");
        return false;
    }

    reconnectAttempts_++;
    LOGD("Reconnect attempt {}.", reconnectAttempts_);

    if (!connectToServer()) {
        LOGE("Failed to reconnect to server");
        std::this_thread::sleep_for(std::chrono::seconds(1));
        return reconnectServer();
    }

    LOGI("Successfully reconnected to server.");

    return true;
}

}  // namespace vision
}  // namespace core
}  // namespace kyai