/*
 * node_device_devkit.c: node device enumeration - DeviceKit-based implementation
 *
 * Copyright (C) 2008 Virtual Iron Software, Inc.
 * Copyright (C) 2008 David F. Lively
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
 *
 * Author: David F. Lively <dlively@virtualiron.com>
 */

#include <config.h>
#include <devkit-gobject.h>

#include "node_device_conf.h"
#include "virterror_internal.h"
#include "driver.h"
#include "datatypes.h"
#include "event.h"
#include "memory.h"
#include "uuid.h"
#include "logging.h"

#include "node_device.h"

/*
 * Host device enumeration (DeviceKit implementation)
 */

static virDeviceMonitorStatePtr driverState;

#define CONN_DRV_STATE(conn) \
        ((virDeviceMonitorStatePtr)((conn)->devMonPrivateData))
#define DRV_STATE_DKCLIENT(ds) ((DevkitClient *)((ds)->privateData))
#define CONN_DKCLIENT(conn) DRV_STATE_DKCLIENT(CONN_DRV_STATE(conn))

#define NODE_DEV_DKDEV(obj) ((DevkitDevice *)((obj)->privateData)

static int get_str_prop(DevkitDevice *dkdev, const char *prop, char **val_p)
{
    char *val = devkit_device_dup_property_as_str(dkdev, prop);

    if (val) {
        if (*val) {
            *val_p = val;
            return 0;
        } else {
            /* Treat empty strings as NULL values */
            VIR_FREE(val);
        }
    }

    return -1;
}

#if 0
static int get_int_prop(DevkitDevice *dkdev, const char *prop, int *val_p)
{
    if (! devkit_device_has_property(dkdev, prop))
        return -1;
    *val_p = devkit_device_get_property_as_int(dkdev, prop);
    return 0;
}

static int get_uint64_prop(DevkitDevice *dkdev, const char *prop,
                           unsigned long long *val_p)
{
    if (! devkit_device_has_property(dkdev, prop))
        return -1;
    *val_p = devkit_device_get_property_as_uint64(dkdev, prop);
    return 0;
}
#endif

static int gather_pci_cap(DevkitDevice *dkdev,
                          union _virNodeDevCapData *d)
{
    const char *sysfs_path = devkit_device_get_native_path(dkdev);

    if (sysfs_path != NULL) {
        char *p = strrchr(sysfs_path, '/');
        if (p) {
            (void)virStrToLong_ui(p+1, &p, 16, &d->pci_dev.domain);
            (void)virStrToLong_ui(p+1, &p, 16, &d->pci_dev.bus);
            (void)virStrToLong_ui(p+1, &p, 16, &d->pci_dev.slot);
            (void)virStrToLong_ui(p+1, &p, 16, &d->pci_dev.function);
        }
    }
    return 0;
}


static int gather_usb_cap(DevkitDevice *dkdev,
                          union _virNodeDevCapData *d)
{
    (void)get_str_prop(dkdev, "ID_VENDOR", &d->usb_dev.vendor_name);
    (void)get_str_prop(dkdev, "ID_MODEL", &d->usb_dev.product_name);
    return 0;
}


static int gather_net_cap(DevkitDevice *dkdev,
                          union _virNodeDevCapData *d)
{
    const char *sysfs_path = devkit_device_get_native_path(dkdev);
    const char *ifname;

    if (sysfs_path == NULL)
        return -1;
    ifname = strrchr(sysfs_path, '/');
    if (!ifname || !*ifname || !*(++ifname))
        return -1;
    if ((d->net.ifname = strdup(ifname)) == NULL)
        return -1;

    d->net.subtype = VIR_NODE_DEV_CAP_NET_LAST;

    return 0;
}


static int gather_storage_cap(DevkitDevice *dkdev,
                              union _virNodeDevCapData *d)
{
    const char *device = devkit_device_get_device_file(dkdev);

    if (device && ((d->storage.block = strdup(device)) == NULL))
        return -1;

    return 0;
}


struct _caps_tbl_entry {
    const char *cap_name;
    enum virNodeDevCapType type;
    int (*gather_fn)(DevkitDevice *dkdev,
                     union _virNodeDevCapData *data);
};

typedef struct _caps_tbl_entry caps_tbl_entry;

static caps_tbl_entry caps_tbl[] = {
    { "pci",        VIR_NODE_DEV_CAP_PCI_DEV,   gather_pci_cap },
    { "usb",        VIR_NODE_DEV_CAP_USB_DEV,   gather_usb_cap },
    { "net",        VIR_NODE_DEV_CAP_NET,       gather_net_cap },
    { "block",      VIR_NODE_DEV_CAP_STORAGE,   gather_storage_cap },
    // TODO: more caps!
};


/* qsort/bsearch string comparator */
static int cmpstringp(const void *p1, const void *p2)
{
    /* from man 3 qsort */
    return strcmp(* (char * const *) p1, * (char * const *) p2);
}


static int gather_capability(DevkitDevice *dkdev,
                             const char *cap_name,
                             virNodeDevCapsDefPtr *caps_p)
{
    size_t caps_tbl_len = sizeof(caps_tbl) / sizeof(caps_tbl[0]);
    caps_tbl_entry *entry;

    entry = bsearch(&cap_name, caps_tbl, caps_tbl_len,
                    sizeof(caps_tbl[0]), cmpstringp);

    if (entry) {
        virNodeDevCapsDefPtr caps;
        if (VIR_ALLOC(caps) < 0)
            return ENOMEM;
        caps->type = entry->type;
        if (entry->gather_fn) {
            int rv = (*entry->gather_fn)(dkdev, &caps->data);
            if (rv != 0) {
                virNodeDevCapsDefFree(caps);
                return rv;
            }
        }
        caps->next = *caps_p;
        *caps_p = caps;
    }

    return 0;
}


static int gather_capabilities(DevkitDevice *dkdev,
                               virNodeDevCapsDefPtr *caps_p)
{
    const char *subsys = devkit_device_get_subsystem(dkdev);
    const char *bus_name = devkit_device_get_property(dkdev, "ID_BUS");
    virNodeDevCapsDefPtr caps = NULL;
    int rv;

    if (subsys) {
        rv = gather_capability(dkdev, subsys, &caps);
        if (rv != 0) goto failure;
    }

    if (bus_name && (subsys == NULL || !STREQ(bus_name, subsys))) {
        rv = gather_capability(dkdev, bus_name, &caps);
        if (rv != 0) goto failure;
    }

    *caps_p = caps;
    return 0;

 failure:
    while (caps) {
        virNodeDevCapsDefPtr next = caps->next;
        virNodeDevCapsDefFree(caps);
        caps = next;
    }
    return rv;
}

static void dev_create(void *_dkdev, void *_dkclient ATTRIBUTE_UNUSED)
{
    DevkitDevice *dkdev = _dkdev;
    const char *sysfs_path = devkit_device_get_native_path(dkdev);
    virNodeDeviceObjPtr dev = NULL;
    virNodeDeviceDefPtr def = NULL;
    const char *name;
    int rv;

    if (sysfs_path == NULL)
        /* Currently using basename(sysfs_path) as device name (key) */
        return;

    name = strrchr(sysfs_path, '/');
    if (name == NULL)
        name = sysfs_path;
    else
        ++name;

    if (VIR_ALLOC(def) < 0)
        goto failure;

    if ((def->name = strdup(name)) == NULL)
        goto failure;

    // TODO: Find device parent, if any

    rv = gather_capabilities(dkdev, &def->caps);
    if (rv != 0) goto failure;

    nodeDeviceLock(driverState);
    dev = virNodeDeviceAssignDef(NULL,
                                 &driverState->devs,
                                 def);

    if (!dev) {
        nodeDeviceUnlock(driverState);
        goto failure;
    }

    dev->privateData = dkdev;
    dev->privateFree = NULL; /* XXX some free func needed ? */
    virNodeDeviceObjUnlock(dev);

    nodeDeviceUnlock(driverState);

    return;

 failure:
    DEBUG("FAILED TO ADD dev %s", name);
    if (def)
        virNodeDeviceDefFree(def);
}


static int devkitDeviceMonitorStartup(int privileged ATTRIBUTE_UNUSED)
{
    size_t caps_tbl_len = sizeof(caps_tbl) / sizeof(caps_tbl[0]);
    DevkitClient *devkit_client = NULL;
    GError *err = NULL;
    GList *devs;
    int i;

    /* Ensure caps_tbl is sorted by capability name */
    qsort(caps_tbl, caps_tbl_len, sizeof(caps_tbl[0]), cmpstringp);

    if (VIR_ALLOC(driverState) < 0)
        return -1;

    if (virMutexInit(&driverState->lock) < 0) {
        VIR_FREE(driverState);
        return -1;
    }

    g_type_init();

    /* Get new devkit_client and connect to daemon */
    devkit_client = devkit_client_new(NULL);
    if (devkit_client == NULL) {
        DEBUG0("devkit_client_new returned NULL");
        goto failure;
    }
    if (!devkit_client_connect(devkit_client, &err)) {
        DEBUG0("devkit_client_connect failed");
        goto failure;
    }

    /* Populate with known devices.
     *
     * This really should be:
        devs = devkit_client_enumerate_by_subsystem(devkit_client, NULL, &err);
        if (err) {
            DEBUG0("devkit_client_enumerate_by_subsystem failed");
            devs = NULL;
            goto failure;
        }
        g_list_foreach(devs, dev_create, devkit_client);
    * but devkit_client_enumerate_by_subsystem currently fails when the second
    * arg is null (contrary to the API documentation).  So the following code
    * (from Dan B) works around this by listing devices per handled subsystem.
    */

    for (i = 0 ; i < ARRAY_CARDINALITY(caps_tbl) ; i++) {
        const char *caps[] = { caps_tbl[i].cap_name, NULL };
        devs = devkit_client_enumerate_by_subsystem(devkit_client,
                                                    caps,
                                                    &err);
        if (err) {
            DEBUG0("devkit_client_enumerate_by_subsystem failed");
            devs = NULL;
            goto failure;
        }
        g_list_foreach(devs, dev_create, devkit_client);
    }

    driverState->privateData = devkit_client;

    // TODO: Register to get DeviceKit events on device changes and
    //       coordinate updates with queries and other operations.

    return 0;

 failure:
    if (err) {
        DEBUG("\terror[%d]: %s", err->code, err->message);
        g_error_free(err);
    }
    if (devs) {
        g_list_foreach(devs, (GFunc)g_object_unref, NULL);
        g_list_free(devs);
    }
    if (devkit_client)
        g_object_unref(devkit_client);
    VIR_FREE(driverState);

    return -1;
}


static int devkitDeviceMonitorShutdown(void)
{
    if (driverState) {
        DevkitClient *devkit_client;

        nodeDeviceLock(driverState);
        devkit_client = DRV_STATE_DKCLIENT(driverState);
        virNodeDeviceObjListFree(&driverState->devs);
        if (devkit_client)
            g_object_unref(devkit_client);
        nodeDeviceUnlock(driverState);
        virMutexDestroy(&driveState->lock);
        VIR_FREE(driverState);
        return 0;
    }
    return -1;
}


static int devkitDeviceMonitorReload(void)
{
    /* XXX This isn't thread safe because its free'ing the thing
     * we're locking */
    (void)devkitDeviceMonitorShutdown();
    return devkitDeviceMonitorStartup();
}


static int devkitDeviceMonitorActive(void)
{
    /* Always ready to deal with a shutdown */
    return 0;
}


static virDrvOpenStatus
devkitNodeDrvOpen(virConnectPtr conn,
                  virConnectAuthPtr auth ATTRIBUTE_UNUSED,
                  int flags ATTRIBUTE_UNUSED)
{
    if (driverState == NULL)
        return VIR_DRV_OPEN_DECLINED;

    conn->devMonPrivateData = driverState;

    return VIR_DRV_OPEN_SUCCESS;
}

static int devkitNodeDrvClose(virConnectPtr conn ATTRIBUTE_UNUSED)
{
    conn->devMonPrivateData = NULL;
    return 0;
}


static virDeviceMonitor devkitDeviceMonitor = {
    .name = "devkitDeviceMonitor",
    .open = devkitNodeDrvOpen,
    .close = devkitNodeDrvClose,
};


static virStateDriver devkitStateDriver = {
    .initialize = devkitDeviceMonitorStartup,
    .cleanup = devkitDeviceMonitorShutdown,
    .reload = devkitDeviceMonitorReload,
    .active = devkitDeviceMonitorActive,
};

int devkitNodeRegister(void)
{
    registerCommonNodeFuncs(&devkitDeviceMonitor);
    if (virRegisterDeviceMonitor(&devkitDeviceMonitor) < 0)
        return -1;
    return virRegisterStateDriver(&devkitStateDriver);
}
