#include "common/dbus.h"

#ifdef FF_HAVE_DBUS

    #include "common/thread.h"
    #include "common/strutil.h"

static bool loadLibSymbols(FFDBusLibrary* lib) {
    FF_LIBRARY_LOAD(dbus, false, "libdbus-1" FF_LIBRARY_EXTENSION, 4);
    FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_bus_get, false)
    FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_message_new_method_call, false)
    FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_message_append_args, false)
    FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_message_iter_init, false)
    FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_message_iter_get_arg_type, false)
    FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_message_iter_get_basic, false)
    FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_message_iter_recurse, false)
    FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_message_iter_has_next, false)
    FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_message_iter_next, false)
    FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_message_unref, false)
    FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_connection_send_with_reply_and_block, false)
    FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_connection_unref, false)
    dbus = nullptr; // don't auto dlclose
    return true;
}

static const FFDBusLibrary* loadLib(void) {
    static FFDBusLibrary lib;
    static bool loaded = false;
    static bool loadSuccess = false;

    if (!loaded) {
        loaded = true;
        loadSuccess = loadLibSymbols(&lib);
    }

    return loadSuccess ? &lib : nullptr;
}

const char* ffDBusLoadData(DBusBusType busType, FFDBusData* data) {
    data->lib = loadLib();
    if (data->lib == nullptr) {
        return "Failed to load DBus library";
    }

    data->connection = data->lib->ffdbus_bus_get(busType, nullptr);
    if (data->connection == nullptr) {
        return "Failed to connect to DBus";
    }

    return nullptr;
}

void ffDBusDestroyData(FFDBusData* data) {
    if (data->connection != nullptr) {
        data->lib->ffdbus_connection_unref(data->connection);
        data->connection = nullptr;
    }
}

bool ffDBusGetString(FFDBusData* dbus, DBusMessageIter* iter, FFstrbuf* result) {
    int argType = dbus->lib->ffdbus_message_iter_get_arg_type(iter);

    if (argType == DBUS_TYPE_STRING || argType == DBUS_TYPE_OBJECT_PATH) {
        const char* value = nullptr;
        dbus->lib->ffdbus_message_iter_get_basic(iter, &value);

        if (!ffStrSet(value)) {
            return false;
        }

        ffStrbufAppendS(result, value);
        return true;
    }

    if (argType == DBUS_TYPE_BYTE) {
        uint8_t value;
        dbus->lib->ffdbus_message_iter_get_basic(iter, &value);
        ffStrbufAppendC(result, (char) value);
        return true;
    }

    if (argType != DBUS_TYPE_VARIANT && argType != DBUS_TYPE_ARRAY) {
        return false;
    }

    DBusMessageIter subIter;
    dbus->lib->ffdbus_message_iter_recurse(iter, &subIter);

    if (argType == DBUS_TYPE_VARIANT) {
        return ffDBusGetString(dbus, &subIter, result);
    }

    // At this point we have an array

    int subArgType = dbus->lib->ffdbus_message_iter_get_arg_type(&subIter);
    if (subArgType == DBUS_TYPE_INVALID) {
        return false;
    }

    if (subArgType == DBUS_TYPE_BYTE) {
        while (true) {
            uint8_t value;
            dbus->lib->ffdbus_message_iter_get_basic(&subIter, &value);
            ffStrbufAppendC(result, (char) value);

            if (!dbus->lib->ffdbus_message_iter_next(&subIter)) {
                break;
            }
        }

        return true;
    }

    bool foundAValue = false;

    while (true) {
        if (ffDBusGetString(dbus, &subIter, result)) {
            foundAValue = true;
            ffStrbufAppendS(result, ", ");
        }

        if (!dbus->lib->ffdbus_message_iter_next(&subIter)) {
            break;
        } else {
            continue;
        }
    }

    if (foundAValue) {
        ffStrbufSubstrBefore(result, result->length - 2);
    }

    return foundAValue;
}

bool ffDBusGetBool(FFDBusData* dbus, DBusMessageIter* iter, bool* result) {
    int argType = dbus->lib->ffdbus_message_iter_get_arg_type(iter);

    if (argType == DBUS_TYPE_BOOLEAN) {
        dbus_bool_t value = 0;
        dbus->lib->ffdbus_message_iter_get_basic(iter, &value);
        *result = value != 0;
        return true;
    }

    if (argType != DBUS_TYPE_VARIANT) {
        return false;
    }

    DBusMessageIter subIter;
    dbus->lib->ffdbus_message_iter_recurse(iter, &subIter);
    return ffDBusGetBool(dbus, &subIter, result);
}

bool ffDBusGetUint(FFDBusData* dbus, DBusMessageIter* iter, uint64_t* result) {
    int argType = dbus->lib->ffdbus_message_iter_get_arg_type(iter);

    if (argType == DBUS_TYPE_BYTE) {
        uint8_t value = 0;
        dbus->lib->ffdbus_message_iter_get_basic(iter, &value);
        *result = value;
        return true;
    }

    if (argType == DBUS_TYPE_UINT16) {
        uint16_t value = 0;
        dbus->lib->ffdbus_message_iter_get_basic(iter, &value);
        *result = value;
        return true;
    }

    if (argType == DBUS_TYPE_UINT32) {
        uint32_t value = 0;
        dbus->lib->ffdbus_message_iter_get_basic(iter, &value);
        *result = value;
        return true;
    }

    if (argType == DBUS_TYPE_UINT64) {
        dbus->lib->ffdbus_message_iter_get_basic(iter, result);
        return true;
    }

    if (argType != DBUS_TYPE_VARIANT) {
        return false;
    }

    DBusMessageIter subIter;
    dbus->lib->ffdbus_message_iter_recurse(iter, &subIter);
    return ffDBusGetUint(dbus, &subIter, result);
}

bool ffDBusGetInt(FFDBusData* dbus, DBusMessageIter* iter, int64_t* result) {
    int argType = dbus->lib->ffdbus_message_iter_get_arg_type(iter);

    if (argType == DBUS_TYPE_INT16) {
        int16_t value = 0;
        dbus->lib->ffdbus_message_iter_get_basic(iter, &value);
        *result = value;
        return true;
    }

    if (argType == DBUS_TYPE_INT32) {
        int32_t value = 0;
        dbus->lib->ffdbus_message_iter_get_basic(iter, &value);
        *result = value;
        return true;
    }

    if (argType == DBUS_TYPE_INT64) {
        dbus->lib->ffdbus_message_iter_get_basic(iter, result);
        return true;
    }

    if (argType == DBUS_TYPE_BYTE) {
        uint8_t value = 0;
        dbus->lib->ffdbus_message_iter_get_basic(iter, &value);
        *result = value;
        return true;
    }

    if (argType == DBUS_TYPE_UINT16) {
        uint16_t value = 0;
        dbus->lib->ffdbus_message_iter_get_basic(iter, &value);
        *result = (int32_t) value;
        return true;
    }

    if (argType == DBUS_TYPE_UINT32) {
        uint32_t value = 0;
        dbus->lib->ffdbus_message_iter_get_basic(iter, &value);
        *result = (int32_t) value;
        return true;
    }

    if (argType == DBUS_TYPE_UINT64) {
        uint64_t value = 0;
        dbus->lib->ffdbus_message_iter_get_basic(iter, &value);
        if (value > INT64_MAX) {
            return false;
        }
        *result = (int64_t) value;
        return true;
    }

    if (argType != DBUS_TYPE_VARIANT) {
        return false;
    }

    DBusMessageIter subIter;
    dbus->lib->ffdbus_message_iter_recurse(iter, &subIter);
    return ffDBusGetInt(dbus, &subIter, result);
}

DBusMessage* ffDBusGetMethodReply(FFDBusData* dbus, const char* busName, const char* objectPath, const char* interface, const char* method, const char* arg1, const char* arg2) {
    DBusMessage* message = dbus->lib->ffdbus_message_new_method_call(busName, objectPath, interface, method);
    if (message == nullptr) {
        return nullptr;
    }

    if (arg1) {
        if (arg2) {
            dbus->lib->ffdbus_message_append_args(message, DBUS_TYPE_STRING, &arg1, DBUS_TYPE_STRING, &arg2, DBUS_TYPE_INVALID);
        } else {
            dbus->lib->ffdbus_message_append_args(message, DBUS_TYPE_STRING, &arg1, DBUS_TYPE_INVALID);
        }
    }

    DBusMessage* reply = dbus->lib->ffdbus_connection_send_with_reply_and_block(dbus->connection, message, instance.config.general.processingTimeout, nullptr);

    dbus->lib->ffdbus_message_unref(message);

    return reply;
}

DBusMessage* ffDBusGetProperty(FFDBusData* dbus, const char* busName, const char* objectPath, const char* interface, const char* property) {
    DBusMessage* message = dbus->lib->ffdbus_message_new_method_call(busName, objectPath, "org.freedesktop.DBus.Properties", "Get");
    if (message == nullptr) {
        return nullptr;
    }

    dbus->lib->ffdbus_message_append_args(message,
        DBUS_TYPE_STRING,
        &interface,
        DBUS_TYPE_STRING,
        &property,
        DBUS_TYPE_INVALID);

    DBusMessage* reply = dbus->lib->ffdbus_connection_send_with_reply_and_block(dbus->connection, message, instance.config.general.processingTimeout, nullptr);

    dbus->lib->ffdbus_message_unref(message);

    return reply;
}

bool ffDBusGetPropertyString(FFDBusData* dbus, const char* busName, const char* objectPath, const char* interface, const char* property, FFstrbuf* result) {
    DBusMessage* reply = ffDBusGetProperty(dbus, busName, objectPath, interface, property);
    if (reply == nullptr) {
        return false;
    }

    DBusMessageIter rootIterator;
    if (!dbus->lib->ffdbus_message_iter_init(reply, &rootIterator)) {
        dbus->lib->ffdbus_message_unref(reply);
        return false;
    }

    bool ret = ffDBusGetString(dbus, &rootIterator, result);

    dbus->lib->ffdbus_message_unref(reply);

    return ret;
}

bool ffDBusGetPropertyUint(FFDBusData* dbus, const char* busName, const char* objectPath, const char* interface, const char* property, uint64_t* result) {
    DBusMessage* reply = ffDBusGetProperty(dbus, busName, objectPath, interface, property);
    if (reply == nullptr) {
        return false;
    }

    DBusMessageIter rootIterator;
    if (!dbus->lib->ffdbus_message_iter_init(reply, &rootIterator)) {
        dbus->lib->ffdbus_message_unref(reply);
        return false;
    }

    bool ret = ffDBusGetUint(dbus, &rootIterator, result);

    dbus->lib->ffdbus_message_unref(reply);

    return ret;
}

#endif // FF_HAVE_DBUS
