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

#ifdef FF_HAVE_DBUS
    #include "common/dbus.h"
    #include "common/io.h"

/* Example dbus reply, striped to only the relevant parts:
array [                                                     //root
    dict entry(                                             //object
        object path "/org/bluez/hci0/dev_03_21_8B_91_16_4D"
        array [
           dict entry(                                      //property
              string "org.bluez.Device1"
              array [
                 dict entry(                                //value
                    string "Address"
                    variant string "03:21:8B:91:16:4D"
                 )
                 dict entry(                                //value
                    string "Name"
                    variant string "JBL TUNE160BT"
                 )
                 dict entry(                                //value
                    string "Icon"
                    variant string "audio-headset"
                 )
                 dict entry(                                //value
                    string "Connected"
                    variant boolean true
                 )
              ]
           )
           dict entry(                                      //property
              string "org.bluez.Battery1"
              array [
                 dict entry(                                //value
                    string "Percentage"
                    variant byte 100
                 )
              ]
           )
        ]
    )
]
*/

static bool detectBluetoothValue(FFDBusData* dbus, DBusMessageIter* iter, FFBluetoothResult* device) {
    if (dbus->lib->ffdbus_message_iter_get_arg_type(iter) != DBUS_TYPE_DICT_ENTRY) {
        return true;
    }

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

    if (dbus->lib->ffdbus_message_iter_get_arg_type(&dictIter) != DBUS_TYPE_STRING) {
        return true;
    }

    const char* deviceProperty;
    dbus->lib->ffdbus_message_iter_get_basic(&dictIter, &deviceProperty);

    dbus->lib->ffdbus_message_iter_next(&dictIter);

    if (ffStrEquals(deviceProperty, "Address")) {
        ffDBusGetString(dbus, &dictIter, &device->address);
    } else if (ffStrEquals(deviceProperty, "Name")) {
        ffDBusGetString(dbus, &dictIter, &device->name);
    } else if (ffStrEquals(deviceProperty, "Icon")) {
        ffDBusGetString(dbus, &dictIter, &device->type);
    } else if (ffStrEquals(deviceProperty, "Percentage")) {
        uint64_t percentage;
        if (ffDBusGetUint(dbus, &dictIter, &percentage)) {
            device->battery = (uint8_t) percentage;
        }
    } else if (ffStrEquals(deviceProperty, "Connected")) {
        ffDBusGetBool(dbus, &dictIter, &device->connected);
    } else if (ffStrEquals(deviceProperty, "Paired")) {
        bool paired = true;
        ffDBusGetBool(dbus, &dictIter, &paired);
        if (!paired) {
            return false;
        }
    }
    return true;
}

static void detectBluetoothProperty(FFDBusData* dbus, DBusMessageIter* iter, FFBluetoothResult* device) {
    if (dbus->lib->ffdbus_message_iter_get_arg_type(iter) != DBUS_TYPE_DICT_ENTRY) {
        return;
    }

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

    if (dbus->lib->ffdbus_message_iter_get_arg_type(&dictIter) != DBUS_TYPE_STRING) {
        return;
    }

    const char* propertyType;
    dbus->lib->ffdbus_message_iter_get_basic(&dictIter, &propertyType);

    if (!ffStrContains(propertyType, ".Device") && !ffStrContains(propertyType, ".Battery")) {
        return; // We don't care about other properties
    }

    dbus->lib->ffdbus_message_iter_next(&dictIter);

    if (dbus->lib->ffdbus_message_iter_get_arg_type(&dictIter) != DBUS_TYPE_ARRAY) {
        return;
    }

    DBusMessageIter arrayIter;
    dbus->lib->ffdbus_message_iter_recurse(&dictIter, &arrayIter);

    do {
        bool shouldContinue = detectBluetoothValue(dbus, &arrayIter, device);
        if (!shouldContinue) {
            ffStrbufClear(&device->name);
            break;
        }
    } while (dbus->lib->ffdbus_message_iter_next(&arrayIter));
}

static FFBluetoothResult* detectBluetoothObject(FFlist* devices, FFDBusData* dbus, DBusMessageIter* iter) {
    if (dbus->lib->ffdbus_message_iter_get_arg_type(iter) != DBUS_TYPE_DICT_ENTRY) {
        return NULL;
    }

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

    if (dbus->lib->ffdbus_message_iter_get_arg_type(&dictIter) != DBUS_TYPE_OBJECT_PATH) {
        return NULL;
    }

    const char* objectPath;
    dbus->lib->ffdbus_message_iter_get_basic(&dictIter, &objectPath);

    // We don't want adapter objects
    if (!ffStrContains(objectPath, "/dev_")) {
        return NULL;
    }

    dbus->lib->ffdbus_message_iter_next(&dictIter);

    if (dbus->lib->ffdbus_message_iter_get_arg_type(&dictIter) != DBUS_TYPE_ARRAY) {
        return NULL;
    }

    DBusMessageIter arrayIter;
    dbus->lib->ffdbus_message_iter_recurse(&dictIter, &arrayIter);

    FFBluetoothResult* device = FF_LIST_ADD(FFBluetoothResult, *devices);
    ffStrbufInit(&device->name);
    ffStrbufInit(&device->address);
    ffStrbufInit(&device->type);
    device->battery = 0;
    device->connected = false;

    do {
        detectBluetoothProperty(dbus, &arrayIter, device);
    } while (dbus->lib->ffdbus_message_iter_next(&arrayIter));

    return device;
}

static void detectBluetoothRoot(FFBluetoothOptions* options, FFlist* devices, FFDBusData* dbus, DBusMessageIter* iter, int32_t connectedCount) {
    if (dbus->lib->ffdbus_message_iter_get_arg_type(iter) != DBUS_TYPE_ARRAY) {
        return;
    }

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

    do {
        FFBluetoothResult* device = detectBluetoothObject(devices, dbus, &arrayIter);

        if (device) {
            if (!options->showDisconnected && !device->connected) {
                ffStrbufDestroy(&device->name);
                ffStrbufDestroy(&device->address);
                ffStrbufDestroy(&device->type);
                --devices->length;
            }

            if (device->name.length == 0) {
                ffStrbufSetStatic(&device->name, "Unknown Device");
            }

            if (device->connected && --connectedCount == 0) {
                break;
            }
        }
    } while (dbus->lib->ffdbus_message_iter_next(&arrayIter));
}

static const char* detectBluetooth(FFBluetoothOptions* options, FFlist* devices, int32_t connectedCount) {
    FF_DBUS_AUTO_DESTROY_DATA FFDBusData dbus = {};
    const char* error = ffDBusLoadData(DBUS_BUS_SYSTEM, &dbus);
    if (error) {
        return error;
    }

    DBusMessage* managedObjects = ffDBusGetMethodReply(&dbus, "org.bluez", "/", "org.freedesktop.DBus.ObjectManager", "GetManagedObjects", NULL, NULL);
    if (!managedObjects) {
        return "Failed to call GetManagedObjects";
    }

    DBusMessageIter rootIter;
    if (!dbus.lib->ffdbus_message_iter_init(managedObjects, &rootIter)) {
        dbus.lib->ffdbus_message_unref(managedObjects);
        return "Failed to get root iterator of GetManagedObjects";
    }

    detectBluetoothRoot(options, devices, &dbus, &rootIter, connectedCount);

    dbus.lib->ffdbus_message_unref(managedObjects);
    return NULL;
}

static uint32_t connectedDevices(void) {
    FF_AUTO_CLOSE_DIR DIR* dirp = opendir("/sys/class/bluetooth");
    if (dirp == NULL) {
        return 0;
    }

    uint32_t result = 0;
    struct dirent* entry;
    while ((entry = readdir(dirp)) != NULL) {
        if (strchr(entry->d_name, ':') != NULL) {
            ++result;
        }
    }

    return result;
}

#endif

const char* ffDetectBluetooth(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */) {
#ifdef FF_HAVE_DBUS
    int32_t connectedCount = -1;
    if (!options->showDisconnected) {
        connectedCount = (int32_t) connectedDevices();
        if (connectedCount == 0) {
            return NULL;
        }
    }

    return detectBluetooth(options, devices, connectedCount);
#else
    FF_UNUSED(options, devices);
    return "Fastfetch was compiled without DBus support";
#endif
}
