/**
 * @file udev.c
 * @brief List of multimedia devices for VLC media player
 */
/*****************************************************************************
 * Copyright © 2009 Rémi Denis-Courmont
 *
 * 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 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., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 ****************************************************************************/

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

#include <libudev.h>
#include <vlc_common.h>
#include <vlc_services_discovery.h>
#include <vlc_plugin.h>
#include <search.h>
#include <poll.h>
#include <errno.h>

static int OpenV4L (vlc_object_t *);
static int OpenALSA (vlc_object_t *);
static int OpenDisc (vlc_object_t *);
static void Close (vlc_object_t *);
static int vlc_sd_probe_Open (vlc_object_t *);

/*
 * Module descriptor
 */
vlc_module_begin ()
    set_shortname (N_("Video capture"))
    set_description (N_("Video capture (Video4Linux)"))
    set_category (CAT_PLAYLIST)
    set_subcategory (SUBCAT_PLAYLIST_SD)
    set_capability ("services_discovery", 0)
    set_callbacks (OpenV4L, Close)
    add_shortcut ("v4l")

    add_submodule ()
    set_shortname (N_("Audio capture"))
    set_description (N_("Audio capture (ALSA)"))
    set_category (CAT_PLAYLIST)
    set_subcategory (SUBCAT_PLAYLIST_SD)
    set_capability ("services_discovery", 0)
    set_callbacks (OpenALSA, Close)
    add_shortcut ("alsa")

    add_submodule ()
    set_shortname (N_("Discs"))
    set_description (N_("Discs"))
    set_category (CAT_PLAYLIST)
    set_subcategory (SUBCAT_PLAYLIST_SD)
    set_capability ("services_discovery", 0)
    set_callbacks (OpenDisc, Close)
    add_shortcut ("disc")

    VLC_SD_PROBE_SUBMODULE

vlc_module_end ()

static int vlc_sd_probe_Open (vlc_object_t *obj)
{
    vlc_probe_t *probe = (vlc_probe_t *)obj;

    struct udev *udev = udev_new ();
    if (udev == NULL)
        return VLC_PROBE_CONTINUE;

    struct udev_monitor *mon = udev_monitor_new_from_netlink (udev, "udev");
    if (mon != NULL)
    {
        vlc_sd_probe_Add (probe, "v4l{longname=\"Video capture\"}",
                          N_("Video capture"), SD_CAT_DEVICES);
        vlc_sd_probe_Add (probe, "alsa{longname=\"Audio capture\"}",
                          N_("Audio capture"), SD_CAT_DEVICES);
        vlc_sd_probe_Add (probe, "disc{longname=\"Discs\"}", N_("Discs"),
                          SD_CAT_DEVICES);
        udev_monitor_unref (mon);
    }
    udev_unref (udev);
    return VLC_PROBE_CONTINUE;
}

struct device
{
    dev_t devnum; /* must be first */
    input_item_t *item;
    services_discovery_t *sd;
};

struct subsys
{
    const char *name;
    char * (*get_mrl) (struct udev_device *dev);
    char * (*get_name) (struct udev_device *dev);
    char * (*get_cat) (struct udev_device *dev);
    int item_type;
};

struct services_discovery_sys_t
{
    const struct subsys *subsys;
    struct udev_monitor *monitor;
    vlc_thread_t         thread;
    void                *root;
};

/**
 * Compares two devices (to support binary search).
 */
static int cmpdev (const void *a, const void *b)
{
    const dev_t *da = a, *db = b;
    dev_t delta = *da - *db;

    if (sizeof (delta) > sizeof (int))
        return delta ? ((delta > 0) ? 1 : -1) : 0;
    return (signed)delta;
}

static void DestroyDevice (void *data)
{
    struct device *d = data;

    if (d->sd)
        services_discovery_RemoveItem (d->sd, d->item);
    vlc_gc_decref (d->item);
    free (d);
}

static char *decode_property (struct udev_device *, const char *);

/**
 * Adds a udev device.
 */
static int AddDevice (services_discovery_t *sd, struct udev_device *dev)
{
    services_discovery_sys_t *p_sys = sd->p_sys;

    char *mrl = p_sys->subsys->get_mrl (dev);
    if (mrl == NULL)
        return 0; /* don't know if it was an error... */
    char *name = p_sys->subsys->get_name (dev);
    input_item_t *item = input_item_NewWithType (VLC_OBJECT (sd), mrl,
                                                 name ? name : mrl,
                                                 0, NULL, 0, -1,
                                                 p_sys->subsys->item_type);
    msg_Dbg (sd, "adding %s (%s)", mrl, name);
    free (name);
    free (mrl);
    if (item == NULL)
        return -1;

    struct device *d = malloc (sizeof (*d));
    if (d == NULL)
    {
        vlc_gc_decref (item);
        return -1;
    }
    d->devnum = udev_device_get_devnum (dev);
    d->item = item;
    d->sd = NULL;

    struct device **dp = tsearch (d, &p_sys->root, cmpdev);
    if (dp == NULL) /* Out-of-memory */
    {
        DestroyDevice (d);
        return -1;
    }
    if (*dp != d) /* Overwrite existing device */
    {
        DestroyDevice (*dp);
        *dp = d;
    }

    name = p_sys->subsys->get_cat (dev);
    services_discovery_AddItem (sd, item, name ? name : "Generic");
    d->sd = sd;
    free (name);
    return 0;
}

/**
 * Removes a udev device (if present).
 */
static void RemoveDevice (services_discovery_t *sd, struct udev_device *dev)
{
    services_discovery_sys_t *p_sys = sd->p_sys;

    dev_t num = udev_device_get_devnum (dev);
    struct device **dp = tfind (&(dev_t){ num }, &p_sys->root, cmpdev);
    if (dp == NULL)
        return;

    struct device *d = *dp;
    tdelete (d, &p_sys->root, cmpdev);
    DestroyDevice (d);
}

static void *Run (void *);

/**
 * Probes and initializes.
 */
static int Open (vlc_object_t *obj, const struct subsys *subsys)
{
    services_discovery_t *sd = (services_discovery_t *)obj;
    services_discovery_sys_t *p_sys = malloc (sizeof (*p_sys));

    if (p_sys == NULL)
        return VLC_ENOMEM;
    sd->p_sys = p_sys;
    p_sys->subsys = subsys;
    p_sys->root = NULL;

    struct udev_monitor *mon = NULL;
    struct udev *udev = udev_new ();
    if (udev == NULL)
        goto error;

    mon = udev_monitor_new_from_netlink (udev, "udev");
    if (mon == NULL
     || udev_monitor_filter_add_match_subsystem_devtype (mon, subsys->name,
                                                         NULL))
        goto error;
    p_sys->monitor = mon;

    /* Enumerate existing devices */
    struct udev_enumerate *devenum = udev_enumerate_new (udev);
    if (devenum == NULL)
        goto error;
    if (udev_enumerate_add_match_subsystem (devenum, subsys->name))
    {
        udev_enumerate_unref (devenum);
        goto error;
    }

    udev_monitor_enable_receiving (mon);
    /* Note that we enumerate _after_ monitoring is enabled so that we do not
     * loose device events occuring while we are enumerating. We could still
     * loose events if the Netlink socket receive buffer overflows. */
    udev_enumerate_scan_devices (devenum);
    struct udev_list_entry *devlist = udev_enumerate_get_list_entry (devenum);
    struct udev_list_entry *deventry;
    udev_list_entry_foreach (deventry, devlist)
    {
        const char *path = udev_list_entry_get_name (deventry);
        struct udev_device *dev = udev_device_new_from_syspath (udev, path);
        AddDevice (sd, dev);
        udev_device_unref (dev);
    }
    udev_enumerate_unref (devenum);

    if (vlc_clone (&p_sys->thread, Run, sd, VLC_THREAD_PRIORITY_LOW))
    {   /* Fallback without thread */
        udev_monitor_unref (mon);
        udev_unref (udev);
        p_sys->monitor = NULL;
    }
    return VLC_SUCCESS;

error:
    if (mon != NULL)
        udev_monitor_unref (mon);
    if (udev != NULL)
        udev_unref (udev);
    free (p_sys);
    return VLC_EGENERIC;
}

/**
 * Releases resources
 */
static void Close (vlc_object_t *obj)
{
    services_discovery_t *sd = (services_discovery_t *)obj;
    services_discovery_sys_t *p_sys = sd->p_sys;

    if (p_sys->monitor != NULL)
    {
        struct udev *udev = udev_monitor_get_udev (p_sys->monitor);

        vlc_cancel (p_sys->thread);
        vlc_join (p_sys->thread, NULL);
        udev_monitor_unref (p_sys->monitor);
        udev_unref (udev);
    }

    tdestroy (p_sys->root, DestroyDevice);
    free (p_sys);
}

static void *Run (void *data)
{
    services_discovery_t *sd = data;
    services_discovery_sys_t *p_sys = sd->p_sys;
    struct udev_monitor *mon = p_sys->monitor;

    int fd = udev_monitor_get_fd (mon);
    struct pollfd ufd = { .fd = fd, .events = POLLIN, };

    for (;;)
    {
        while (poll (&ufd, 1, -1) == -1)
            if (errno != EINTR)
                break;

        int canc = vlc_savecancel ();
        struct udev_device *dev = udev_monitor_receive_device (mon);
        if (dev == NULL)
            continue;

        const char *action = udev_device_get_action (dev);
        if (!strcmp (action, "add"))
            AddDevice (sd, dev);
        else if (!strcmp (action, "remove"))
            RemoveDevice (sd, dev);
        else if (!strcmp (action, "change"))
        {
            RemoveDevice (sd, dev);
            AddDevice (sd, dev);
        }
        udev_device_unref (dev);
        vlc_restorecancel (canc);
    }
    return NULL;
}

/**
 * Converts an hexadecimal digit to an integer.
 */
static int hex (char c)
{
    if (c >= '0' && c <= '9')
        return c - '0';
    if (c >= 'A' && c <= 'F')
        return c + 10 - 'A';
    if (c >= 'a' && c <= 'f')
        return c + 10 - 'a';
    return -1;
}

/**
 * Decodes a udev hexadecimal-encoded property.
 */
static char *decode (const char *enc)
{
    char *ret = enc ? strdup (enc) : NULL;
    if (ret == NULL)
        return NULL;

    char *out = ret;
    for (const char *in = ret; *in; out++)
    {
        int h1, h2;

        if ((in[0] == '\\') && (in[1] == 'x')
         && ((h1 = hex (in[2])) != -1)
         && ((h2 = hex (in[3])) != -1))
        {
            *out = (h1 << 4) | h2;
            in += 4;
        }
        else
        {
            *out = *in;
            in++;
        }
    }
    *out = 0;
    return ret;
}

static char *decode_property (struct udev_device *dev, const char *name)
{
    return decode (udev_device_get_property_value (dev, name));
}


/*** Video4Linux support ***/
static bool is_v4l_legacy (struct udev_device *dev)
{
    const char *version;

    version = udev_device_get_property_value (dev, "ID_V4L_VERSION");
    return version && !strcmp (version, "1");
}

static char *v4l_get_mrl (struct udev_device *dev)
{
    /* Determine media location */
    const char *scheme = "v4l2";
    if (is_v4l_legacy (dev))
        scheme = "v4l";
    const char *node = udev_device_get_devnode (dev);
    char *mrl;

    if (asprintf (&mrl, "%s://%s", scheme, node) == -1)
        mrl = NULL;
    return mrl;
}

static char *v4l_get_name (struct udev_device *dev)
{
    const char *prd = udev_device_get_property_value (dev, "ID_V4L_PRODUCT");
    return prd ? strdup (prd) : NULL;
}

static char *v4l_get_cat (struct udev_device *dev)
{
    return decode_property (dev, "ID_VENDOR_ENC");
}

int OpenV4L (vlc_object_t *obj)
{
    static const struct subsys subsys = {
        "video4linux", v4l_get_mrl, v4l_get_name, v4l_get_cat, ITEM_TYPE_CARD,
    };

    return Open (obj, &subsys);
}


/*** Advanced Linux Sound Architecture support ***/
static int alsa_get_device (struct udev_device *dev, unsigned *restrict pcard,
                            unsigned *restrict pdevice)
{
    const char *node = udev_device_get_devpath (dev);
    char type;

    node = strrchr (node, '/');
    if (node == NULL)
        return -1;
    if (sscanf (node, "/pcmC%uD%u%c", pcard, pdevice, &type) < 3)
        return -1;
    if (type != 'c')
        return -1;
    return 0;
}


static char *alsa_get_mrl (struct udev_device *dev)
{
    /* Determine media location */
    char *mrl;
    unsigned card, device;

    if (alsa_get_device (dev, &card, &device))
        return NULL;

    if (asprintf (&mrl, "alsa://plughw:%u,%u", card, device) == -1)
        mrl = NULL;
    return mrl;
}

static char *alsa_get_name (struct udev_device *dev)
{
    const char *model = NULL;
    char *name;
    unsigned card, device;

    if (alsa_get_device (dev, &card, &device))
        return NULL;

    dev = udev_device_get_parent (dev);
    if (dev != NULL)
        model = udev_device_get_property_value (dev,
                                                "ID_MODEL_FROM_DATABASE");
    if (model == NULL)
        model = _("Device");

    if (asprintf (&name, "%s (%u)", model, device) == -1)
        name = NULL;
    return name;
}

static char *alsa_get_cat (struct udev_device *dev)
{
    const char *vnd;

    dev = udev_device_get_parent (dev);
    if (dev == NULL)
        return NULL;

    vnd = udev_device_get_property_value (dev, "ID_VENDOR_FROM_DATABASE");
    if (vnd == NULL)
        /* FIXME: USB may take time to settle... the parent device */
        vnd = udev_device_get_property_value (dev, "ID_BUS");
    return vnd ? strdup (vnd) : NULL;
}

int OpenALSA (vlc_object_t *obj)
{
    static const struct subsys subsys = {
        "sound", alsa_get_mrl, alsa_get_name, alsa_get_cat, ITEM_TYPE_CARD,
    };

    return Open (obj, &subsys);
}


/*** Discs support ***/
static char *disc_get_mrl (struct udev_device *dev)
{
    const char *val;

    val = udev_device_get_property_value (dev, "ID_CDROM");
    if (val == NULL)
        return NULL; /* Ignore non-optical block devices */

    val = udev_device_get_property_value (dev, "ID_CDROM_MEDIA_STATE");
    if (val && !strcmp (val, "blank"))
        return NULL; /* ignore empty drives and virgin recordable discs */

    const char *scheme = "invalid";
    val = udev_device_get_property_value (dev,
                                          "ID_CDROM_MEDIA_TRACK_COUNT_AUDIO");
    if (val && atoi (val))
        scheme = "cdda"; /* Audio CD rather than file system */
    val = udev_device_get_property_value (dev, "ID_CDROM_MEDIA_DVD");
    if (val && atoi (val))
        scheme = "file";

    val = udev_device_get_property_value (dev, "ID_CDROM_MEDIA_BD");
    if (val && atoi (val))
        scheme = "bd";
#ifdef LOL
    val = udev_device_get_property_value (dev, "ID_CDROM_MEDIA_HDDVD");
    if (val && atoi (val))
        scheme = "hddvd";
#endif

    /* We didn't get any property that could tell we have optical disc
       that we can play */
    if( !strcmp( scheme, "invalid" ) )
        return NULL;

    val = udev_device_get_devnode (dev);
    char *mrl;

    if (asprintf (&mrl, "%s://%s", scheme, val) == -1)
        mrl = NULL;
    return mrl;
}

static char *disc_get_name (struct udev_device *dev)
{
    return decode_property (dev, "ID_FS_LABEL_ENC");
}

static char *disc_get_cat (struct udev_device *dev)
{
    struct udev_list_entry *list, *entry;

    list = udev_device_get_properties_list_entry (dev);
    if (unlikely(list == NULL))
        return NULL;

    const char *cat = NULL;
    udev_list_entry_foreach (entry, list)
    {
        const char *name = udev_list_entry_get_name (entry);

        if (strncmp (name, "ID_CDROM_MEDIA_", 15))
            continue;
        if (!atoi (udev_list_entry_get_value (entry)))
            continue;
        name += 15;

        if (!strncmp (name, "CD", 2))
            cat = N_("CD");
        else if (!strncmp (name, "DVD", 3))
            cat = N_("DVD");
        else if (!strncmp (name, "BD", 2))
            cat = N_("Blu-Ray");
        else if (!strncmp (name, "HDDVD", 5))
            cat = N_("HD DVD");

        if (cat != NULL)
            break;
    }

    if (cat == NULL)
        cat = N_("Unknown type");
    return strdup (vlc_gettext (cat));
}

int OpenDisc (vlc_object_t *obj)
{
    static const struct subsys subsys = {
        "block", disc_get_mrl, disc_get_name, disc_get_cat, ITEM_TYPE_DISC,
    };

    return Open (obj, &subsys);
}
