/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * 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 2 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:
 *
 * Copyright (C) 2016 Velocloud, Inc.
 */

#include <string.h>

#define _LIBMM_INSIDE_MM
#include <libmm-glib.h>

#include <ModemManager-tags.h>

#include "mm-kernel-device-udev.h"
#include "mm-log-object.h"

static void initable_iface_init (GInitableIface *iface);

G_DEFINE_TYPE_EXTENDED (MMKernelDeviceUdev, mm_kernel_device_udev,  MM_TYPE_KERNEL_DEVICE, 0,
                        G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init))

enum {
    PROP_0,
    PROP_UDEV_DEVICE,
    PROP_PROPERTIES,
    PROP_LAST
};

static GParamSpec *properties[PROP_LAST];

struct _MMKernelDeviceUdevPrivate {
    GUdevDevice *device;
    GUdevDevice *interface;
    GUdevDevice *physdev;
    guint16      vendor;
    guint16      product;
    guint16      revision;

    MMKernelEventProperties *properties;
};

/*****************************************************************************/

static gboolean
get_device_ids (GUdevDevice *device,
                guint16     *vendor,
                guint16     *product,
                guint16     *revision)
{
    GUdevDevice *parent = NULL;
    const gchar *vid = NULL, *pid = NULL, *rid = NULL, *parent_subsys;
    gboolean success = FALSE;
    char *pci_vid = NULL, *pci_pid = NULL;

    g_assert (vendor != NULL && product != NULL);

    parent = g_udev_device_get_parent (device);
    if (parent) {
        parent_subsys = g_udev_device_get_subsystem (parent);
        if (parent_subsys) {
            if (g_str_equal (parent_subsys, "bluetooth")) {
                /* Bluetooth devices report the VID/PID of the BT adapter here,
                 * which isn't really what we want.  Just return null IDs instead.
                 */
                success = TRUE;
                goto out;
            } else if (g_str_equal (parent_subsys, "pcmcia")) {
                /* For PCMCIA devices we need to grab the PCMCIA subsystem's
                 * manfid and cardid, since any IDs on the tty device itself
                 * may be from PCMCIA controller or something else.
                 */
                vid = g_udev_device_get_sysfs_attr (parent, "manf_id");
                pid = g_udev_device_get_sysfs_attr (parent, "card_id");
                if (!vid || !pid)
                    goto out;
            } else if (g_str_equal (parent_subsys, "platform")) {
                /* Platform devices don't usually have a VID/PID */
                success = TRUE;
                goto out;
            } else if (g_str_has_prefix (parent_subsys, "usb") &&
                       (!g_strcmp0 (g_udev_device_get_driver (parent), "qmi_wwan") ||
                        !g_strcmp0 (g_udev_device_get_driver (parent), "cdc_mbim"))) {
                /* Need to look for vendor/product in the parent of the QMI/MBIM device */
                GUdevDevice *qmi_parent;

                qmi_parent = g_udev_device_get_parent (parent);
                if (qmi_parent) {
                    vid = g_udev_device_get_property (qmi_parent, "ID_VENDOR_ID");
                    pid = g_udev_device_get_property (qmi_parent, "ID_MODEL_ID");
                    rid = g_udev_device_get_property (qmi_parent, "ID_REVISION");
                    g_object_unref (qmi_parent);
                }
            } else if (g_str_equal (parent_subsys, "pci")) {
                const char *pci_id;

                /* We can't always rely on the model + vendor showing up on
                 * the PCI device's child, so look at the PCI parent.  PCI_ID
                 * has the format "1931:000C".
                 */
                pci_id = g_udev_device_get_property (parent, "PCI_ID");
                if (pci_id && strlen (pci_id) == 9 && pci_id[4] == ':') {
                    vid = pci_vid = g_strdup (pci_id);
                    pci_vid[4] = '\0';
                    pid = pci_pid = g_strdup (pci_id + 5);
                }
            }
        }
    }

    if (!vid)
        vid = g_udev_device_get_property (device, "ID_VENDOR_ID");
    if (!vid)
        goto out;

    if (strncmp (vid, "0x", 2) == 0)
        vid += 2;
    if (strlen (vid) != 4)
        goto out;

    if (!pid)
        pid = g_udev_device_get_property (device, "ID_MODEL_ID");
    if (!pid)
        goto out;

    if (strncmp (pid, "0x", 2) == 0)
        pid += 2;
    if (strlen (pid) != 4)
        goto out;

    *vendor = (guint16) (mm_utils_hex2byte (vid + 2) & 0xFF);
    *vendor |= (guint16) ((mm_utils_hex2byte (vid) & 0xFF) << 8);

    *product = (guint16) (mm_utils_hex2byte (pid + 2) & 0xFF);
    *product |= (guint16) ((mm_utils_hex2byte (pid) & 0xFF) << 8);


    /* Revision ID optional, default to 0x0000 if unknown */
    *revision = 0;
    if (!rid)
        rid = g_udev_device_get_property (device, "ID_REVISION");
    if (rid) {
        if (strncmp (rid, "0x", 2) == 0)
            rid += 2;
        if (strlen (rid) == 4) {
            *revision = (guint16) (mm_utils_hex2byte (rid + 2) & 0xFF);
            *revision |= (guint16) ((mm_utils_hex2byte (rid) & 0xFF) << 8);
        }
    }

    success = TRUE;

out:
    if (parent)
        g_object_unref (parent);
    g_free (pci_vid);
    g_free (pci_pid);
    return success;
}

static void
ensure_device_ids (MMKernelDeviceUdev *self)
{
    /* Revision is optional */
    if (self->priv->vendor || self->priv->product)
        return;

    if (!self->priv->device)
        return;

    if (!get_device_ids (self->priv->device, &self->priv->vendor, &self->priv->product, &self->priv->revision))
        mm_obj_dbg (self, "could not get vendor/product id");
}

/*****************************************************************************/

static GUdevDevice *
find_physical_gudevdevice (GUdevDevice *child)
{
    GUdevDevice *iter, *old = NULL;
    GUdevDevice *physdev = NULL;
    const char *subsys, *type, *name;
    guint32 i = 0;
    gboolean is_usb = FALSE, is_pcmcia = FALSE;

    g_return_val_if_fail (child != NULL, NULL);

    /* Bluetooth rfcomm devices are "virtual" and don't necessarily have
     * parents at all.
     */
    name = g_udev_device_get_name (child);
    if (name && strncmp (name, "rfcomm", 6) == 0)
        return g_object_ref (child);

    iter = g_object_ref (child);
    while (iter && i++ < 8) {
        subsys = g_udev_device_get_subsystem (iter);
        if (subsys) {
            if (is_usb || g_str_has_prefix (subsys, "usb")) {
                is_usb = TRUE;
                type = g_udev_device_get_devtype (iter);
                if (type && !strcmp (type, "usb_device")) {
                    physdev = iter;
                    break;
                }
            } else if (is_pcmcia || !strcmp (subsys, "pcmcia")) {
                GUdevDevice *pcmcia_parent;
                const char *tmp_subsys;

                is_pcmcia = TRUE;

                /* If the parent of this PCMCIA device is no longer part of
                 * the PCMCIA subsystem, we want to stop since we're looking
                 * for the base PCMCIA device, not the PCMCIA controller which
                 * is usually PCI or some other bus type.
                 */
                pcmcia_parent = g_udev_device_get_parent (iter);
                if (pcmcia_parent) {
                    tmp_subsys = g_udev_device_get_subsystem (pcmcia_parent);
                    if (tmp_subsys && strcmp (tmp_subsys, "pcmcia"))
                        physdev = iter;
                    g_object_unref (pcmcia_parent);
                    if (physdev)
                        break;
                }
            } else if (!strcmp (subsys, "platform") ||
                       !strcmp (subsys, "pci") ||
                       !strcmp (subsys, "pnp") ||
                       !strcmp (subsys, "sdio")) {
                /* Take the first parent as the physical device */
                physdev = iter;
                break;
            }
        }

        old = iter;
        iter = g_udev_device_get_parent (old);
        g_object_unref (old);
    }

    return physdev;
}

static void
ensure_physdev (MMKernelDeviceUdev *self)
{
    if (self->priv->physdev)
        return;
    if (self->priv->device)
        self->priv->physdev = find_physical_gudevdevice (self->priv->device);
}

/*****************************************************************************/

static void
ensure_interface (MMKernelDeviceUdev *self)
{
    GUdevDevice *new_parent;
    GUdevDevice *parent;

    if (self->priv->interface)
        return;

    if (!self->priv->device)
        return;

    ensure_physdev (self);

    parent = g_udev_device_get_parent (self->priv->device);
    while (1) {
        /* Abort if no parent found */
        if (!parent)
            break;

        /* Look for the first parent that is a USB interface (i.e. has
         * bInterfaceClass) */
        if (g_udev_device_has_sysfs_attr (parent, "bInterfaceClass")) {
            self->priv->interface = parent;
            break;
        }

        /* If unknown physdev, just stop right away */
        if (!self->priv->physdev || parent == self->priv->physdev) {
            g_object_unref (parent);
            break;
        }

        new_parent = g_udev_device_get_parent (parent);
        g_object_unref (parent);
        parent = new_parent;
    }
}

/*****************************************************************************/

static const gchar *
kernel_device_get_subsystem (MMKernelDevice *_self)
{
    MMKernelDeviceUdev *self;

    g_return_val_if_fail (MM_IS_KERNEL_DEVICE_UDEV (_self), NULL);

    self = MM_KERNEL_DEVICE_UDEV (_self);

    if (self->priv->device)
        return g_udev_device_get_subsystem (self->priv->device);

    g_assert (self->priv->properties);
    return mm_kernel_event_properties_get_subsystem (self->priv->properties);
}

static const gchar *
kernel_device_get_name (MMKernelDevice *_self)
{
    MMKernelDeviceUdev *self;

    g_return_val_if_fail (MM_IS_KERNEL_DEVICE_UDEV (_self), NULL);

    self = MM_KERNEL_DEVICE_UDEV (_self);

    if (self->priv->device)
        return g_udev_device_get_name (self->priv->device);

    g_assert (self->priv->properties);
    return mm_kernel_event_properties_get_name (self->priv->properties);
}

static const gchar *
kernel_device_get_driver (MMKernelDevice *_self)
{
    MMKernelDeviceUdev *self;
    const gchar *driver, *subsys, *name;

    g_return_val_if_fail (MM_IS_KERNEL_DEVICE_UDEV (_self), NULL);

    self = MM_KERNEL_DEVICE_UDEV (_self);

    if (!self->priv->device)
        return NULL;

    driver = g_udev_device_get_driver (self->priv->device);
    if (!driver) {
        GUdevDevice *parent;

        parent = g_udev_device_get_parent (self->priv->device);
        if (parent)
            driver = g_udev_device_get_driver (parent);

        /* Check for bluetooth; it's driver is a bunch of levels up so we
         * just check for the subsystem of the parent being bluetooth.
         */
        if (!driver && parent) {
            subsys = g_udev_device_get_subsystem (parent);
            if (subsys && !strcmp (subsys, "bluetooth"))
                driver = "bluetooth";
        }

        if (parent)
            g_object_unref (parent);
    }

    /* Newer kernels don't set up the rfcomm port parent in sysfs,
     * so we must infer it from the device name.
     */
    name = g_udev_device_get_name (self->priv->device);
    if (!driver && strncmp (name, "rfcomm", 6) == 0)
        driver = "bluetooth";

    /* Note: may return NULL! */
    return driver;
}

static const gchar *
kernel_device_get_sysfs_path (MMKernelDevice *self)
{
    g_return_val_if_fail (MM_IS_KERNEL_DEVICE (self), NULL);

    if (!MM_KERNEL_DEVICE_UDEV (self)->priv->device)
        return NULL;

    return g_udev_device_get_sysfs_path (MM_KERNEL_DEVICE_UDEV (self)->priv->device);
}

static const gchar *
kernel_device_get_physdev_uid (MMKernelDevice *_self)
{
    MMKernelDeviceUdev *self;
    const gchar *uid = NULL;

    g_return_val_if_fail (MM_IS_KERNEL_DEVICE_UDEV (_self), NULL);

    self = MM_KERNEL_DEVICE_UDEV (_self);

    /* Prefer the one coming in the properties, if any */
    if (self->priv->properties) {
        if ((uid = mm_kernel_event_properties_get_uid (self->priv->properties)) != NULL)
            return uid;
    }

    /* Try to load from properties set on the physical device */
    if ((uid = mm_kernel_device_get_global_property (MM_KERNEL_DEVICE (self), ID_MM_PHYSDEV_UID)) != NULL)
        return uid;

    /* Use physical device sysfs path, if any */
    if (self->priv->physdev && (uid = g_udev_device_get_sysfs_path (self->priv->physdev)) != NULL)
        return uid;

    /* If there is no physical device sysfs path, use the device sysfs itself */
    g_assert (self->priv->device);
    return g_udev_device_get_sysfs_path (self->priv->device);
}

static guint16
kernel_device_get_physdev_vid (MMKernelDevice *_self)
{
    MMKernelDeviceUdev *self;

    g_return_val_if_fail (MM_IS_KERNEL_DEVICE_UDEV (_self), 0);

    self = MM_KERNEL_DEVICE_UDEV (_self);
    ensure_device_ids (self);
    return self->priv->vendor;
}

static guint16
kernel_device_get_physdev_pid (MMKernelDevice *_self)
{
    MMKernelDeviceUdev *self;

    g_return_val_if_fail (MM_IS_KERNEL_DEVICE_UDEV (_self), 0);

    self = MM_KERNEL_DEVICE_UDEV (_self);
    ensure_device_ids (self);
    return self->priv->product;
}

static guint16
kernel_device_get_physdev_revision (MMKernelDevice *_self)
{
    MMKernelDeviceUdev *self;

    g_return_val_if_fail (MM_IS_KERNEL_DEVICE_UDEV (_self), 0);

    self = MM_KERNEL_DEVICE_UDEV (_self);
    ensure_device_ids (self);
    return self->priv->revision;
}

static const gchar *
kernel_device_get_physdev_sysfs_path (MMKernelDevice *_self)
{
    MMKernelDeviceUdev *self;

    g_return_val_if_fail (MM_IS_KERNEL_DEVICE_UDEV (_self), NULL);

    self = MM_KERNEL_DEVICE_UDEV (_self);
    ensure_physdev (self);
    if (!self->priv->physdev)
        return NULL;

    return g_udev_device_get_sysfs_path (self->priv->physdev);
}

static const gchar *
kernel_device_get_physdev_subsystem (MMKernelDevice *_self)
{
    MMKernelDeviceUdev *self;

    g_return_val_if_fail (MM_IS_KERNEL_DEVICE_UDEV (_self), NULL);

    self = MM_KERNEL_DEVICE_UDEV (_self);
    ensure_physdev (self);
    if (!self->priv->physdev)
        return NULL;

    return g_udev_device_get_subsystem (self->priv->physdev);
}

static const gchar *
kernel_device_get_physdev_manufacturer (MMKernelDevice *_self)
{
    MMKernelDeviceUdev *self;

    g_return_val_if_fail (MM_IS_KERNEL_DEVICE_UDEV (_self), NULL);

    self = MM_KERNEL_DEVICE_UDEV (_self);
    ensure_physdev (self);
    if (!self->priv->physdev)
        return NULL;

    return g_udev_device_get_sysfs_attr (self->priv->physdev, "manufacturer");
}

static const gchar *
kernel_device_get_physdev_product (MMKernelDevice *_self)
{
    MMKernelDeviceUdev *self;

    g_return_val_if_fail (MM_IS_KERNEL_DEVICE_UDEV (_self), NULL);

    self = MM_KERNEL_DEVICE_UDEV (_self);
    ensure_physdev (self);
    if (!self->priv->physdev)
        return NULL;

    return g_udev_device_get_sysfs_attr (self->priv->physdev, "product");
}

static gint
kernel_device_get_interface_class (MMKernelDevice *_self)
{
    MMKernelDeviceUdev *self;

    g_return_val_if_fail (MM_IS_KERNEL_DEVICE_UDEV (_self), -1);

    self = MM_KERNEL_DEVICE_UDEV (_self);
    ensure_interface (self);
    return (self->priv->interface ? g_udev_device_get_sysfs_attr_as_int (self->priv->interface, "bInterfaceClass") : -1);
}

static gint
kernel_device_get_interface_subclass (MMKernelDevice *_self)
{
    MMKernelDeviceUdev *self;

    g_return_val_if_fail (MM_IS_KERNEL_DEVICE_UDEV (_self), -1);

    self = MM_KERNEL_DEVICE_UDEV (_self);
    ensure_interface (self);
    return (self->priv->interface ? g_udev_device_get_sysfs_attr_as_int (self->priv->interface, "bInterfaceSubClass") : -1);
}

static gint
kernel_device_get_interface_protocol (MMKernelDevice *_self)
{
    MMKernelDeviceUdev *self;

    g_return_val_if_fail (MM_IS_KERNEL_DEVICE_UDEV (_self), -1);

    self = MM_KERNEL_DEVICE_UDEV (_self);
    ensure_interface (self);
    return (self->priv->interface ? g_udev_device_get_sysfs_attr_as_int (self->priv->interface, "bInterfaceProtocol") : -1);
}

static const gchar *
kernel_device_get_interface_sysfs_path (MMKernelDevice *_self)
{
    MMKernelDeviceUdev *self;

    g_return_val_if_fail (MM_IS_KERNEL_DEVICE_UDEV (_self), NULL);

    self = MM_KERNEL_DEVICE_UDEV (_self);
    ensure_interface (self);
    return (self->priv->interface ? g_udev_device_get_sysfs_path (self->priv->interface) : NULL);
}

static const gchar *
kernel_device_get_interface_description (MMKernelDevice *_self)
{
    MMKernelDeviceUdev *self;

    g_return_val_if_fail (MM_IS_KERNEL_DEVICE_UDEV (_self), NULL);

    self = MM_KERNEL_DEVICE_UDEV (_self);
    ensure_interface (self);
    return (self->priv->interface ? g_udev_device_get_sysfs_attr (self->priv->interface, "interface") : NULL);
}

static gboolean
kernel_device_cmp (MMKernelDevice *_a,
                   MMKernelDevice *_b)
{
    MMKernelDeviceUdev *a;
    MMKernelDeviceUdev *b;

    g_return_val_if_fail (MM_IS_KERNEL_DEVICE_UDEV (_a), FALSE);
    g_return_val_if_fail (MM_IS_KERNEL_DEVICE_UDEV (_b), FALSE);

    a = MM_KERNEL_DEVICE_UDEV (_a);
    b = MM_KERNEL_DEVICE_UDEV (_b);

    if (a->priv->device && b->priv->device) {
        if (g_udev_device_has_property (a->priv->device, "DEVPATH_OLD") &&
            g_str_has_suffix (g_udev_device_get_sysfs_path (b->priv->device),
                              g_udev_device_get_property   (a->priv->device, "DEVPATH_OLD")))
            return TRUE;

        if (g_udev_device_has_property (b->priv->device, "DEVPATH_OLD") &&
            g_str_has_suffix (g_udev_device_get_sysfs_path (a->priv->device),
                              g_udev_device_get_property   (b->priv->device, "DEVPATH_OLD")))
            return TRUE;

        return !g_strcmp0 (g_udev_device_get_sysfs_path (a->priv->device), g_udev_device_get_sysfs_path (b->priv->device));
    }

    return (!g_strcmp0 (mm_kernel_device_get_subsystem (_a), mm_kernel_device_get_subsystem (_b)) &&
            !g_strcmp0 (mm_kernel_device_get_name      (_a), mm_kernel_device_get_name      (_b)));
}

static gboolean
kernel_device_has_property (MMKernelDevice *_self,
                            const gchar    *property)
{
    MMKernelDeviceUdev *self;

    g_return_val_if_fail (MM_IS_KERNEL_DEVICE_UDEV (_self), FALSE);

    self = MM_KERNEL_DEVICE_UDEV (_self);

    if (!self->priv->device)
        return FALSE;

    return g_udev_device_has_property (self->priv->device, property);
}

static const gchar *
kernel_device_get_property (MMKernelDevice *_self,
                            const gchar    *property)
{
    MMKernelDeviceUdev *self;

    g_return_val_if_fail (MM_IS_KERNEL_DEVICE_UDEV (_self), NULL);

    self = MM_KERNEL_DEVICE_UDEV (_self);

    if (!self->priv->device)
        return NULL;

    return g_udev_device_get_property (self->priv->device, property);
}

static gboolean
kernel_device_get_property_as_boolean (MMKernelDevice *_self,
                                       const gchar    *property)
{
    MMKernelDeviceUdev *self;

    g_return_val_if_fail (MM_IS_KERNEL_DEVICE_UDEV (_self), FALSE);

    self = MM_KERNEL_DEVICE_UDEV (_self);

    if (!self->priv->device)
        return FALSE;

    return g_udev_device_get_property_as_boolean (self->priv->device, property);
}

static gint
kernel_device_get_property_as_int (MMKernelDevice *_self,
                                   const gchar    *property)
{
    MMKernelDeviceUdev *self;

    g_return_val_if_fail (MM_IS_KERNEL_DEVICE_UDEV (_self), -1);

    self = MM_KERNEL_DEVICE_UDEV (_self);

    if (!self->priv->device)
        return -1;

    return g_udev_device_get_property_as_int (self->priv->device, property);
}

static guint
kernel_device_get_property_as_int_hex (MMKernelDevice *_self,
                                       const gchar    *property)
{
    MMKernelDeviceUdev *self;
    const gchar        *s;
    guint               out = 0;

    g_return_val_if_fail (MM_IS_KERNEL_DEVICE_UDEV (_self), G_MAXUINT);

    self = MM_KERNEL_DEVICE_UDEV (_self);

    if (!self->priv->device)
        return G_MAXUINT;

    s = g_udev_device_get_property (self->priv->device, property);
    return ((s && mm_get_uint_from_hex_str (s, &out)) ? out : 0);
}

static gboolean
kernel_device_has_global_property (MMKernelDevice *_self,
                                   const gchar    *property)
{
    MMKernelDeviceUdev *self;

    g_return_val_if_fail (MM_IS_KERNEL_DEVICE_UDEV (_self), FALSE);

    self = MM_KERNEL_DEVICE_UDEV (_self);

    ensure_physdev (self);
    if (self->priv->physdev && g_udev_device_has_property (self->priv->physdev, property))
        return TRUE;

    return kernel_device_has_property (_self, property);
}

static const gchar *
kernel_device_get_global_property (MMKernelDevice *_self,
                                   const gchar    *property)
{
    MMKernelDeviceUdev *self;
    const gchar        *str;

    g_return_val_if_fail (MM_IS_KERNEL_DEVICE_UDEV (_self), NULL);

    self = MM_KERNEL_DEVICE_UDEV (_self);

    ensure_physdev (self);
    if (self->priv->physdev &&
        g_udev_device_has_property (self->priv->physdev, property) &&
        (str = g_udev_device_get_property (self->priv->physdev, property)) != NULL)
        return str;

    return kernel_device_get_property (_self, property);
}

static gboolean
kernel_device_get_global_property_as_boolean (MMKernelDevice *_self,
                                              const gchar    *property)
{
    MMKernelDeviceUdev *self;

    g_return_val_if_fail (MM_IS_KERNEL_DEVICE_UDEV (_self), FALSE);

    self = MM_KERNEL_DEVICE_UDEV (_self);

    ensure_physdev (self);
    if (self->priv->physdev &&
        g_udev_device_has_property (self->priv->physdev, property) &&
        g_udev_device_get_property (self->priv->physdev, property))
        return TRUE;

    return kernel_device_get_property_as_boolean (_self, property);
}

static gint
kernel_device_get_global_property_as_int (MMKernelDevice *_self,
                                          const gchar    *property)
{
    MMKernelDeviceUdev *self;
    gint                value;

    g_return_val_if_fail (MM_IS_KERNEL_DEVICE_UDEV (_self), -1);

    self = MM_KERNEL_DEVICE_UDEV (_self);

    ensure_physdev (self);
    if (self->priv->physdev &&
        g_udev_device_has_property (self->priv->physdev, property) &&
        (value = g_udev_device_get_property_as_int (self->priv->physdev, property)) >= 0)
        return value;

    return kernel_device_get_property_as_int (_self, property);
}

static guint
kernel_device_get_global_property_as_int_hex (MMKernelDevice *_self,
                                              const gchar    *property)
{
    MMKernelDeviceUdev *self;
    const gchar        *s;
    guint               out = 0;

    g_return_val_if_fail (MM_IS_KERNEL_DEVICE_UDEV (_self), G_MAXUINT);

    self = MM_KERNEL_DEVICE_UDEV (_self);

    ensure_physdev (self);
    if (self->priv->physdev &&
        g_udev_device_has_property (self->priv->physdev, property) &&
        (s = g_udev_device_get_property (self->priv->physdev, property)) != NULL)
        return ((s && mm_get_uint_from_hex_str (s, &out)) ? out : 0);

    return kernel_device_get_property_as_int_hex (_self, property);
}

/*****************************************************************************/

MMKernelDevice *
mm_kernel_device_udev_new (GUdevDevice *udev_device)
{
    GError *error = NULL;
    MMKernelDevice *self;

    g_return_val_if_fail (G_UDEV_IS_DEVICE (udev_device), NULL);

    self = MM_KERNEL_DEVICE (g_initable_new (MM_TYPE_KERNEL_DEVICE_UDEV,
                                             NULL,
                                             &error,
                                             "udev-device", udev_device,
                                             NULL));
    g_assert_no_error (error);
    return self;
}

/*****************************************************************************/

MMKernelDevice *
mm_kernel_device_udev_new_from_properties (MMKernelEventProperties  *props,
                                           GError                  **error)
{
    return MM_KERNEL_DEVICE (g_initable_new (MM_TYPE_KERNEL_DEVICE_UDEV,
                                             NULL,
                                             error,
                                             "properties", props,
                                             NULL));
}

/*****************************************************************************/

static void
mm_kernel_device_udev_init (MMKernelDeviceUdev *self)
{
    /* Initialize private data */
    self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, MM_TYPE_KERNEL_DEVICE_UDEV, MMKernelDeviceUdevPrivate);
}

static void
set_property (GObject      *object,
              guint         prop_id,
              const GValue *value,
              GParamSpec   *pspec)
{
    MMKernelDeviceUdev *self = MM_KERNEL_DEVICE_UDEV (object);

    switch (prop_id) {
    case PROP_UDEV_DEVICE:
        g_assert (!self->priv->device);
        self->priv->device = g_value_dup_object (value);
        break;
    case PROP_PROPERTIES:
        g_assert (!self->priv->properties);
        self->priv->properties = g_value_dup_object (value);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
        break;
    }
}

static void
get_property (GObject    *object,
              guint       prop_id,
              GValue     *value,
              GParamSpec *pspec)
{
    MMKernelDeviceUdev *self = MM_KERNEL_DEVICE_UDEV (object);

    switch (prop_id) {
    case PROP_UDEV_DEVICE:
        g_value_set_object (value, self->priv->device);
        break;
    case PROP_PROPERTIES:
        g_value_set_object (value, self->priv->properties);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
        break;
    }
}

static gboolean
initable_init (GInitable     *initable,
               GCancellable  *cancellable,
               GError       **error)
{
    MMKernelDeviceUdev *self = MM_KERNEL_DEVICE_UDEV (initable);
    const gchar *subsystem;
    const gchar *name;

    /* When created from a GUdevDevice, we're done */
    if (self->priv->device)
        return TRUE;

    /* Otherwise, we do need properties with subsystem and name */
    if (!self->priv->properties) {
        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS,
                     "missing properties in kernel device");
        return FALSE;
    }

    subsystem = mm_kernel_event_properties_get_subsystem (self->priv->properties);
    if (!subsystem) {
        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS,
                     "subsystem is mandatory in kernel device");
        return FALSE;
    }

    name = mm_kernel_event_properties_get_name (self->priv->properties);
    if (!mm_kernel_device_get_name (MM_KERNEL_DEVICE (self))) {
        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS,
                     "name is mandatory in kernel device");
        return FALSE;
    }

    /* On remove events, we don't look for the GUdevDevice */
    if (g_strcmp0 (mm_kernel_event_properties_get_action (self->priv->properties), "remove")) {
        GUdevClient *client;
        GUdevDevice *device;

        client = g_udev_client_new (NULL);
        device = g_udev_client_query_by_subsystem_and_name (client, subsystem, name);
        if (!device) {
            g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS,
                         "device %s/%s not found",
                         subsystem,
                         name);
            g_object_unref (client);
            return FALSE;
        }

        /* Store device */
        self->priv->device = device;
        g_object_unref (client);
    }

    return TRUE;
}

static void
dispose (GObject *object)
{
    MMKernelDeviceUdev *self = MM_KERNEL_DEVICE_UDEV (object);

    g_clear_object (&self->priv->physdev);
    g_clear_object (&self->priv->interface);
    g_clear_object (&self->priv->device);
    g_clear_object (&self->priv->properties);

    G_OBJECT_CLASS (mm_kernel_device_udev_parent_class)->dispose (object);
}

static void
initable_iface_init (GInitableIface *iface)
{
    iface->init = initable_init;
}

static void
mm_kernel_device_udev_class_init (MMKernelDeviceUdevClass *klass)
{
    GObjectClass        *object_class        = G_OBJECT_CLASS (klass);
    MMKernelDeviceClass *kernel_device_class = MM_KERNEL_DEVICE_CLASS (klass);

    g_type_class_add_private (object_class, sizeof (MMKernelDeviceUdevPrivate));

    object_class->dispose      = dispose;
    object_class->get_property = get_property;
    object_class->set_property = set_property;

    kernel_device_class->get_subsystem                  = kernel_device_get_subsystem;
    kernel_device_class->get_name                       = kernel_device_get_name;
    kernel_device_class->get_driver                     = kernel_device_get_driver;
    kernel_device_class->get_sysfs_path                 = kernel_device_get_sysfs_path;
    kernel_device_class->get_physdev_uid                = kernel_device_get_physdev_uid;
    kernel_device_class->get_physdev_vid                = kernel_device_get_physdev_vid;
    kernel_device_class->get_physdev_pid                = kernel_device_get_physdev_pid;
    kernel_device_class->get_physdev_revision           = kernel_device_get_physdev_revision;
    kernel_device_class->get_physdev_sysfs_path         = kernel_device_get_physdev_sysfs_path;
    kernel_device_class->get_physdev_subsystem          = kernel_device_get_physdev_subsystem;
    kernel_device_class->get_physdev_manufacturer       = kernel_device_get_physdev_manufacturer;
    kernel_device_class->get_physdev_product            = kernel_device_get_physdev_product;
    kernel_device_class->get_interface_class            = kernel_device_get_interface_class;
    kernel_device_class->get_interface_subclass         = kernel_device_get_interface_subclass;
    kernel_device_class->get_interface_protocol         = kernel_device_get_interface_protocol;
    kernel_device_class->get_interface_sysfs_path       = kernel_device_get_interface_sysfs_path;
    kernel_device_class->get_interface_description      = kernel_device_get_interface_description;
    kernel_device_class->cmp                            = kernel_device_cmp;
    kernel_device_class->has_property                   = kernel_device_has_property;
    kernel_device_class->get_property                   = kernel_device_get_property;
    kernel_device_class->get_property_as_boolean        = kernel_device_get_property_as_boolean;
    kernel_device_class->get_property_as_int            = kernel_device_get_property_as_int;
    kernel_device_class->get_property_as_int_hex        = kernel_device_get_property_as_int_hex;
    kernel_device_class->has_global_property            = kernel_device_has_global_property;
    kernel_device_class->get_global_property            = kernel_device_get_global_property;
    kernel_device_class->get_global_property_as_boolean = kernel_device_get_global_property_as_boolean;
    kernel_device_class->get_global_property_as_int     = kernel_device_get_global_property_as_int;
    kernel_device_class->get_global_property_as_int_hex = kernel_device_get_global_property_as_int_hex;

    properties[PROP_UDEV_DEVICE] =
        g_param_spec_object ("udev-device",
                             "udev device",
                             "Device object as reported by GUdev",
                             G_UDEV_TYPE_DEVICE,
                             G_PARAM_READWRITE);
    g_object_class_install_property (object_class, PROP_UDEV_DEVICE, properties[PROP_UDEV_DEVICE]);

    properties[PROP_PROPERTIES] =
        g_param_spec_object ("properties",
                             "Properties",
                             "Generic kernel event properties",
                             MM_TYPE_KERNEL_EVENT_PROPERTIES,
                             G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
    g_object_class_install_property (object_class, PROP_PROPERTIES, properties[PROP_PROPERTIES]);
}
