/*****************************************************************************
 * controls.c : Video4Linux2 device controls for vlc
 *****************************************************************************
 * Copyright (C) 2002-2011 VLC authors and VideoLAN
 *
 * Authors: Benjamin Pracht <bigben at videolan dot org>
 *          Richard Hosking <richard at hovis dot net>
 *          Antoine Cellerier <dionoea at videolan d.t org>
 *          Dennis Lou <dlou99 at yahoo dot com>
 *
 * This program 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 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
 *****************************************************************************/

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include <stdio.h>
#include <ctype.h>
#include <assert.h>
#include <errno.h>
#include <sys/ioctl.h>

#include <vlc_common.h>

#include "v4l2.h"

typedef struct vlc_v4l2_ctrl_name
{
    const char name[28];
    uint32_t cid;
} vlc_v4l2_ctrl_name_t;

/* NOTE: must be sorted by ID */
static const vlc_v4l2_ctrl_name_t controls[] =
{
    { "brightness", V4L2_CID_BRIGHTNESS },
    { "contrast", V4L2_CID_CONTRAST },
    { "saturation", V4L2_CID_SATURATION },
    { "hue", V4L2_CID_HUE },
    { "audio-volume", V4L2_CID_AUDIO_VOLUME },
    { "audio-balance", V4L2_CID_AUDIO_BALANCE },
    { "audio-bass", V4L2_CID_AUDIO_BASS },
    { "audio-treble", V4L2_CID_AUDIO_TREBLE },
    { "audio-mute", V4L2_CID_AUDIO_MUTE },
    { "audio-loudness", V4L2_CID_AUDIO_LOUDNESS },
    { "auto-white-balance", V4L2_CID_AUTO_WHITE_BALANCE },
    { "do-white-balance", V4L2_CID_DO_WHITE_BALANCE },
    { "red-balance", V4L2_CID_RED_BALANCE },
    { "blue-balance", V4L2_CID_BLUE_BALANCE },
    { "gamma", V4L2_CID_GAMMA },
    { "autogain", V4L2_CID_AUTOGAIN },
    { "gain", V4L2_CID_GAIN },
    { "hflip", V4L2_CID_HFLIP },
    { "vflip", V4L2_CID_VFLIP },
    { "power-line-frequency", V4L2_CID_POWER_LINE_FREQUENCY },
    { "hue-auto", V4L2_CID_HUE_AUTO },
    { "white-balance-temperature", V4L2_CID_WHITE_BALANCE_TEMPERATURE },
    { "sharpness", V4L2_CID_SHARPNESS },
    { "backlight-compensation", V4L2_CID_BACKLIGHT_COMPENSATION },
    { "chroma-gain-auto", V4L2_CID_CHROMA_AGC },
    { "color-killer", V4L2_CID_COLOR_KILLER },
    { "color-effect", V4L2_CID_COLORFX },
    { "rotate", V4L2_CID_ROTATE },
    { "bg-color", V4L2_CID_BG_COLOR }, // NOTE: output only
    { "chroma-gain", V4L2_CID_CHROMA_GAIN },
    { "brightness-auto", V4L2_CID_AUTOBRIGHTNESS },
    { "band-stop-filter", V4L2_CID_BAND_STOP_FILTER },

    { "illuminators-1", V4L2_CID_ILLUMINATORS_1 }, // NOTE: don't care?
    { "illuminators-2", V4L2_CID_ILLUMINATORS_2 },
#define CTRL_CID_KNOWN(cid) \
    ((((uint32_t)cid) - V4L2_CID_BRIGHTNESS) \
        <= (V4L2_CID_BAND_STOP_FILTER - V4L2_CID_BRIGHTNESS))
};

struct vlc_v4l2_ctrl
{
    int                   fd;
    uint32_t              id;
    uint8_t               type;
    char                  name[32];
    int32_t               default_value;
    struct vlc_v4l2_ctrl *next;
};

static int ControlSet (const vlc_v4l2_ctrl_t *c, int_fast32_t value)
{
    struct v4l2_control ctrl = {
        .id = c->id,
        .value = value,
    };
    if (v4l2_ioctl (c->fd, VIDIOC_S_CTRL, &ctrl) < 0)
        return -1;
    return 0;
}

static int ControlSet64 (const vlc_v4l2_ctrl_t *c, int64_t value)
{
    struct v4l2_ext_control ext_ctrl = {
        .id = c->id,
        .size = 0,
    };
    ext_ctrl.value64 = value;
    struct v4l2_ext_controls ext_ctrls = {
        .ctrl_class = V4L2_CTRL_ID2CLASS(c->id),
        .count = 1,
        .error_idx = 0,
        .controls = &ext_ctrl,
    };

    if (v4l2_ioctl (c->fd, VIDIOC_S_EXT_CTRLS, &ext_ctrls) < 0)
        return -1;
    return 0;
}

static int ControlSetStr (const vlc_v4l2_ctrl_t *c, const char *restrict value)
{
    struct v4l2_ext_control ext_ctrl = {
        .id = c->id,
        .size = strlen (value) + 1,
    };
    ext_ctrl.string = (char *)value;
    struct v4l2_ext_controls ext_ctrls = {
        .ctrl_class = V4L2_CTRL_ID2CLASS(c->id),
        .count = 1,
        .error_idx = 0,
        .controls = &ext_ctrl,
    };

    if (v4l2_ioctl (c->fd, VIDIOC_S_EXT_CTRLS, &ext_ctrls) < 0)
        return -1;
    return 0;
}

static int ControlSetCallback (vlc_object_t *obj, const char *var,
                               vlc_value_t old, vlc_value_t cur, void *data)
{
    const vlc_v4l2_ctrl_t *ctrl = data;
    int ret;

    switch (ctrl->type)
    {
        case V4L2_CTRL_TYPE_INTEGER:
        case V4L2_CTRL_TYPE_MENU:
        case V4L2_CTRL_TYPE_BITMASK:
        case V4L2_CTRL_TYPE_INTEGER_MENU:
            ret = ControlSet (ctrl, cur.i_int);
            break;
        case V4L2_CTRL_TYPE_BOOLEAN:
            ret = ControlSet (ctrl, cur.b_bool);
            break;
        case V4L2_CTRL_TYPE_BUTTON:
            ret = ControlSet (ctrl, 0);
            break;
        case V4L2_CTRL_TYPE_INTEGER64:
            ret = ControlSet64 (ctrl, cur.i_int);
            break;
        case V4L2_CTRL_TYPE_STRING:
            ret = ControlSetStr (ctrl, cur.psz_string);
            break;
        default:
            assert (0);
    }

    if (ret)
    {
        msg_Err (obj, "cannot set control %s: %s", var, vlc_strerror_c(errno));
        return VLC_EGENERIC;
    }
    (void) old;
    return VLC_SUCCESS;
}

static void ControlsReset (vlc_object_t *obj, vlc_v4l2_ctrl_t *list)
{
    while (list != NULL)
    {
        switch (list->type)
        {
            case V4L2_CTRL_TYPE_INTEGER:
            case V4L2_CTRL_TYPE_MENU:
            case V4L2_CTRL_TYPE_INTEGER_MENU:
                var_SetInteger (obj, list->name, list->default_value);
                break;
            case V4L2_CTRL_TYPE_BOOLEAN:
                var_SetBool (obj, list->name, list->default_value);
                break;
            default:;
        }
        list = list->next;
    }
}

static int ControlsResetCallback (vlc_object_t *obj, const char *var,
                                  vlc_value_t old, vlc_value_t cur, void *data)
{
    ControlsReset (obj, data);
    (void) var; (void) old; (void) cur;
    return VLC_SUCCESS;
}

static void ControlsSetFromString (vlc_object_t *obj,
                                   const vlc_v4l2_ctrl_t *list)
{
    char *buf = var_InheritString (obj, CFG_PREFIX"set-ctrls");
    if (buf == NULL)
        return;

    char *p = buf;
    if (*p == '{')
        p++;

    char *end = strchr (p, '}');
    if (end != NULL)
        *end = '\0';
next:
    while (p != NULL && *p)
    {
        const char *name, *value;

        p += strspn (p, ", ");
        name = p;
        end = strchr (p, ',');
        if (end != NULL)
            *(end++) = '\0';
        p = end; /* next name/value pair */

        end = strchr (name, '=');
        if (end == NULL)
        {
            /* TODO? support button controls that way? */
            msg_Err (obj, "syntax error in \"%s\": missing '='", name);
            continue;
        }
        *(end++) = '\0';
        value = end;

        for (const vlc_v4l2_ctrl_t *c = list; c != NULL; c = c->next)
            if (!strcasecmp (name, c->name))
                switch (c->type)
                {
                    case V4L2_CTRL_TYPE_INTEGER:
                    case V4L2_CTRL_TYPE_BOOLEAN:
                    case V4L2_CTRL_TYPE_MENU:
                    case V4L2_CTRL_TYPE_INTEGER_MENU:
                    {
                        long val = strtol (value, &end, 0);
                        if (*end)
                        {
                            msg_Err (obj, "syntax error in \"%s\": "
                                     " not an integer", value);
                            goto next;
                        }
                        ControlSet (c, val);
                        break;
                    }

                    case V4L2_CTRL_TYPE_INTEGER64:
                    {
                        long long val = strtoll (value, &end, 0);
                        if (*end)
                        {
                            msg_Err (obj, "syntax error in \"%s\": "
                                     " not an integer", value);
                            goto next;
                        }
                        ControlSet64 (c, val);
                        break;
                    }

                    case V4L2_CTRL_TYPE_STRING:
                        ControlSetStr (c, value);
                        break;

                    case V4L2_CTRL_TYPE_BITMASK:
                    {
                        unsigned long val = strtoul (value, &end, 0);
                        if (*end)
                        {
                            msg_Err (obj, "syntax error in \"%s\": "
                                     " not an integer", value);
                            goto next;
                        }
                        ControlSet (c, val);
                        break;
                    }

                    default:
                        msg_Err (obj, "setting \"%s\" not supported", name);
                        goto next;
                }

        msg_Err (obj, "control \"%s\" not available", name);
    }
    free (buf);
}

static int cidcmp (const void *a, const void *b)
{
    const uint32_t *id = a;
    const vlc_v4l2_ctrl_name_t *name = b;

    return (int32_t)(*id - name->cid);
}

/**
 * Creates a VLC-V4L2 control structure:
 * In particular, determines a name suitable for a VLC object variable.
 * \param query V4L2 control query structure [IN]
 * \return NULL on error
 */
static vlc_v4l2_ctrl_t *ControlCreate (int fd,
                                       const struct v4l2_queryctrl *query)
{
    vlc_v4l2_ctrl_t *ctrl = malloc (sizeof (*ctrl));
    if (unlikely(ctrl == NULL))
        return NULL;

    ctrl->fd = fd;
    ctrl->id = query->id;
    ctrl->type = query->type;

    /* Search for a well-known control */
    const vlc_v4l2_ctrl_name_t *known;
    known = bsearch (&query->id, controls, sizeof (controls) / sizeof (*known),
                     sizeof (*known), cidcmp);
    if (known != NULL)
        strcpy (ctrl->name, known->name);
    else
    /* Fallback to automatically-generated control name */
    {
        size_t i;
        for (i = 0; query->name[i]; i++)
        {
            unsigned char c = query->name[i];
            if (c == ' ' || c == ',')
                c = '_';
            if (c < 128)
                c = tolower (c);
            ctrl->name[i] = c;
        }
        ctrl->name[i] = '\0';
    }

    ctrl->default_value = query->default_value;
    return ctrl;
}


#define CTRL_FLAGS_IGNORE \
    (V4L2_CTRL_FLAG_DISABLED /* not implemented at all */ \
    |V4L2_CTRL_FLAG_READ_ONLY /* value is constant */ \
    |V4L2_CTRL_FLAG_VOLATILE /* value is (variable but) read-only */)

static vlc_v4l2_ctrl_t *ControlAddInteger (vlc_object_t *obj, int fd,
                                           const struct v4l2_queryctrl *query)
{
    msg_Dbg (obj, " integer  %s (%08"PRIX32")", query->name, query->id);
    if (query->flags & CTRL_FLAGS_IGNORE)
        return NULL;

    vlc_v4l2_ctrl_t *c = ControlCreate (fd, query);
    if (unlikely(c == NULL))
        return NULL;

    if (var_Create (obj, c->name, VLC_VAR_INTEGER | VLC_VAR_ISCOMMAND))
    {
        free (c);
        return NULL;
    }

    vlc_value_t val;
    struct v4l2_control ctrl = { .id = query->id };

    if (v4l2_ioctl (fd, VIDIOC_G_CTRL, &ctrl) >= 0)
    {
        msg_Dbg (obj, "  current: %3"PRId32", default: %3"PRId32,
                 ctrl.value, query->default_value);
        val.i_int = ctrl.value;
        var_Change (obj, c->name, VLC_VAR_SETVALUE, &val, NULL);
    }
    val.i_int = query->minimum;
    var_Change (obj, c->name, VLC_VAR_SETMIN, &val, NULL);
    val.i_int = query->maximum;
    var_Change (obj, c->name, VLC_VAR_SETMAX, &val, NULL);
    if (query->step != 1)
    {
        val.i_int = query->step;
        var_Change (obj, c->name, VLC_VAR_SETSTEP, &val, NULL);
    }
    val.i_int = query->default_value;
    var_Change (obj, c->name, VLC_VAR_SETDEFAULT, &val, NULL);
    return c;
}

static vlc_v4l2_ctrl_t *ControlAddBoolean (vlc_object_t *obj, int fd,
                                           const struct v4l2_queryctrl *query)
{
    msg_Dbg (obj, " boolean  %s (%08"PRIX32")", query->name, query->id);
    if (query->flags & CTRL_FLAGS_IGNORE)
        return NULL;

    vlc_v4l2_ctrl_t *c = ControlCreate (fd, query);
    if (unlikely(c == NULL))
        return NULL;

    if (var_Create (obj, c->name, VLC_VAR_BOOL | VLC_VAR_ISCOMMAND))
    {
        free (c);
        return NULL;
    }

    vlc_value_t val;
    struct v4l2_control ctrl = { .id = query->id };

    if (v4l2_ioctl (fd, VIDIOC_G_CTRL, &ctrl) >= 0)
    {
        msg_Dbg (obj, "  current: %s, default: %s",
                 ctrl.value ? " true" : "false",
                 query->default_value ? " true" : "false");
        val.b_bool = ctrl.value;
        var_Change (obj, c->name, VLC_VAR_SETVALUE, &val, NULL);
    }
    val.b_bool = query->default_value;
    var_Change (obj, c->name, VLC_VAR_SETDEFAULT, &val, NULL);
    return c;
}

static vlc_v4l2_ctrl_t *ControlAddMenu (vlc_object_t *obj, int fd,
                                        const struct v4l2_queryctrl *query)
{
    msg_Dbg (obj, " menu     %s (%08"PRIX32")", query->name, query->id);
    if (query->flags & CTRL_FLAGS_IGNORE)
        return NULL;

    vlc_v4l2_ctrl_t *c = ControlCreate (fd, query);
    if (unlikely(c == NULL))
        return NULL;

    if (var_Create (obj, c->name, VLC_VAR_INTEGER | VLC_VAR_HASCHOICE
                                                  | VLC_VAR_ISCOMMAND))
    {
        free (c);
        return NULL;
    }

    vlc_value_t val;
    struct v4l2_control ctrl = { .id = query->id };

    if (v4l2_ioctl (fd, VIDIOC_G_CTRL, &ctrl) >= 0)
    {
        msg_Dbg (obj, "  current: %"PRId32", default: %"PRId32,
                 ctrl.value, query->default_value);
        val.i_int = ctrl.value;
        var_Change (obj, c->name, VLC_VAR_SETVALUE, &val, NULL);
    }
    val.i_int = query->minimum;
    var_Change (obj, c->name, VLC_VAR_SETMIN, &val, NULL);
    val.i_int = query->maximum;
    var_Change (obj, c->name, VLC_VAR_SETMAX, &val, NULL);
    val.i_int = query->default_value;
    var_Change (obj, c->name, VLC_VAR_SETDEFAULT, &val, NULL);

    /* Import menu choices */
    for (uint_fast32_t idx = query->minimum;
         idx <= (uint_fast32_t)query->maximum;
         idx++)
    {
        struct v4l2_querymenu menu = { .id = query->id, .index = idx };

        if (v4l2_ioctl (fd, VIDIOC_QUERYMENU, &menu) < 0)
            continue;
        msg_Dbg (obj, "  choice %"PRIu32") %s", menu.index, menu.name);

        vlc_value_t text;
        val.i_int = menu.index;
        text.psz_string = (char *)menu.name;
        var_Change (obj, c->name, VLC_VAR_ADDCHOICE, &val, &text);
    }
    return c;
}

static vlc_v4l2_ctrl_t *ControlAddButton (vlc_object_t *obj, int fd,
                                          const struct v4l2_queryctrl *query)
{
    msg_Dbg (obj, " button   %s (%08"PRIX32")", query->name, query->id);
    if (query->flags & CTRL_FLAGS_IGNORE)
        return NULL;

    vlc_v4l2_ctrl_t *c = ControlCreate (fd, query);
    if (unlikely(c == NULL))
        return NULL;

    if (var_Create (obj, c->name, VLC_VAR_VOID | VLC_VAR_ISCOMMAND))
    {
        free (c);
        return NULL;
    }
    return c;
}

static vlc_v4l2_ctrl_t *ControlAddInteger64 (vlc_object_t *obj, int fd,
                                            const struct v4l2_queryctrl *query)
{
    msg_Dbg (obj, " 64-bits  %s (%08"PRIX32")", query->name, query->id);
    if (query->flags & CTRL_FLAGS_IGNORE)
        return NULL;

    vlc_v4l2_ctrl_t *c = ControlCreate (fd, query);
    if (unlikely(c == NULL))
        return NULL;

    if (var_Create (obj, c->name, VLC_VAR_INTEGER | VLC_VAR_ISCOMMAND))
    {
        free (c);
        return NULL;
    }

    struct v4l2_ext_control ext_ctrl = { .id = c->id, .size = 0, };
    struct v4l2_ext_controls ext_ctrls = {
        .ctrl_class = V4L2_CTRL_ID2CLASS(c->id),
        .count = 1,
        .error_idx = 0,
        .controls = &ext_ctrl,
    };

    if (v4l2_ioctl (c->fd, VIDIOC_G_EXT_CTRLS, &ext_ctrls) >= 0)
    {
        vlc_value_t val = { .i_int = ext_ctrl.value64 };

        msg_Dbg (obj, "  current: %"PRId64, val.i_int);
        var_Change (obj, c->name, VLC_VAR_SETVALUE, &val, NULL);
    }

    return c;
}

static vlc_v4l2_ctrl_t *ControlAddClass (vlc_object_t *obj, int fd,
                                         const struct v4l2_queryctrl *query)
{
    msg_Dbg (obj, "control class %s:", query->name);
    (void) fd;
    return NULL;
}

static vlc_v4l2_ctrl_t *ControlAddString (vlc_object_t *obj, int fd,
                                          const struct v4l2_queryctrl *query)
{
    msg_Dbg (obj, " string   %s (%08"PRIX32")", query->name, query->id);
    if ((query->flags & CTRL_FLAGS_IGNORE) || query->maximum > 65535)
        return NULL;

    vlc_v4l2_ctrl_t *c = ControlCreate (fd, query);
    if (unlikely(c == NULL))
        return NULL;

    if (var_Create (obj, c->name, VLC_VAR_STRING | VLC_VAR_ISCOMMAND))
    {
        free (c);
        return NULL;
    }

    /* Get current value */
    char *buf = malloc (query->maximum + 1);
    if (likely(buf != NULL))
    {
        struct v4l2_ext_control ext_ctrl = {
            .id = c->id,
            .size = query->maximum + 1,
        };
        ext_ctrl.string = buf;
        struct v4l2_ext_controls ext_ctrls = {
            .ctrl_class = V4L2_CTRL_ID2CLASS(c->id),
            .count = 1,
            .error_idx = 0,
            .controls = &ext_ctrl,
        };

        if (v4l2_ioctl (c->fd, VIDIOC_G_EXT_CTRLS, &ext_ctrls) >= 0)
        {
            vlc_value_t val = { .psz_string = buf };

            msg_Dbg (obj, "  current: \"%s\"", buf);
            var_Change (obj, c->name, VLC_VAR_SETVALUE, &val, NULL);
        }
        free (buf);
    }

    return c;
}

static vlc_v4l2_ctrl_t *ControlAddBitMask (vlc_object_t *obj, int fd,
                                           const struct v4l2_queryctrl *query)
{
    msg_Dbg (obj, " bit mask %s (%08"PRIX32")", query->name, query->id);
    if (query->flags & CTRL_FLAGS_IGNORE)
        return NULL;

    vlc_v4l2_ctrl_t *c = ControlCreate (fd, query);
    if (unlikely(c == NULL))
        return NULL;

    if (var_Create (obj, c->name, VLC_VAR_INTEGER | VLC_VAR_ISCOMMAND))
    {
        free (c);
        return NULL;
    }

    vlc_value_t val;
    struct v4l2_control ctrl = { .id = query->id };

    if (v4l2_ioctl (fd, VIDIOC_G_CTRL, &ctrl) >= 0)
    {
        msg_Dbg (obj, "  current: 0x%08"PRIX32", default: 0x%08"PRIX32,
                 ctrl.value, query->default_value);
        val.i_int = ctrl.value;
        var_Change (obj, c->name, VLC_VAR_SETVALUE, &val, NULL);
    }
    val.i_int = 0;
    var_Change (obj, c->name, VLC_VAR_SETMIN, &val, NULL);
    val.i_int = (uint32_t)query->maximum;
    var_Change (obj, c->name, VLC_VAR_SETMAX, &val, NULL);
    val.i_int = query->default_value;
    var_Change (obj, c->name, VLC_VAR_SETDEFAULT, &val, NULL);
    return c;
}

static vlc_v4l2_ctrl_t *ControlAddIntMenu (vlc_object_t *obj, int fd,
                                           const struct v4l2_queryctrl *query)
{
    msg_Dbg (obj, " int menu %s (%08"PRIX32")", query->name, query->id);
    if (query->flags & CTRL_FLAGS_IGNORE)
        return NULL;

    vlc_v4l2_ctrl_t *c = ControlCreate (fd, query);
    if (unlikely(c == NULL))
        return NULL;

    if (var_Create (obj, c->name, VLC_VAR_INTEGER | VLC_VAR_HASCHOICE
                                                  | VLC_VAR_ISCOMMAND))
    {
        free (c);
        return NULL;
    }

    vlc_value_t val;
    struct v4l2_control ctrl = { .id = query->id };

    if (v4l2_ioctl (fd, VIDIOC_G_CTRL, &ctrl) >= 0)
    {
        msg_Dbg (obj, "  current: %"PRId32", default: %"PRId32,
                 ctrl.value, query->default_value);
        val.i_int = ctrl.value;
        var_Change (obj, c->name, VLC_VAR_SETVALUE, &val, NULL);
    }
    val.i_int = query->minimum;
    var_Change (obj, c->name, VLC_VAR_SETMIN, &val, NULL);
    val.i_int = query->maximum;
    var_Change (obj, c->name, VLC_VAR_SETMAX, &val, NULL);
    val.i_int = query->default_value;
    var_Change (obj, c->name, VLC_VAR_SETDEFAULT, &val, NULL);

    /* Import menu choices */
    for (uint_fast32_t idx = query->minimum;
         idx <= (uint_fast32_t)query->maximum;
         idx++)
    {
        struct v4l2_querymenu menu = { .id = query->id, .index = idx };
        char name[sizeof ("-9223372036854775808")];

        if (v4l2_ioctl (fd, VIDIOC_QUERYMENU, &menu) < 0)
            continue;
        msg_Dbg (obj, "  choice %"PRIu32") %"PRId64, menu.index,
                 (uint64_t)menu.value);

        vlc_value_t text;
        val.i_int = menu.index;
        sprintf (name, "%"PRId64, (int64_t)menu.value);
        text.psz_string = name;
        var_Change (obj, c->name, VLC_VAR_ADDCHOICE, &val, &text);
    }
    return c;
}

static vlc_v4l2_ctrl_t *ControlAddUnknown (vlc_object_t *obj, int fd,
                                           const struct v4l2_queryctrl *query)
{
    msg_Dbg (obj, " unknown %s (%08"PRIX32")", query->name, query->id);
    msg_Warn (obj, "  unknown control type %u", (unsigned)query->type);
    (void) fd;
    return NULL;
}

typedef vlc_v4l2_ctrl_t *(*ctrl_type_cb) (vlc_object_t *, int,
                                          const struct v4l2_queryctrl *);

/**
 * Lists all user-class v4l2 controls, sets them to the user specified
 * value and create the relevant variables to enable run-time changes.
 */
vlc_v4l2_ctrl_t *ControlsInit (vlc_object_t *obj, int fd)
{
    /* A list of controls that can be modified at run-time is stored in the
     * "controls" variable. The V4L2 controls dialog can be built from this. */
    var_Create (obj, "controls", VLC_VAR_INTEGER | VLC_VAR_HASCHOICE);

    static const ctrl_type_cb handlers[] =
    {
        [V4L2_CTRL_TYPE_INTEGER] = ControlAddInteger,
        [V4L2_CTRL_TYPE_BOOLEAN] = ControlAddBoolean,
        [V4L2_CTRL_TYPE_MENU] = ControlAddMenu,
        [V4L2_CTRL_TYPE_BUTTON] = ControlAddButton,
        [V4L2_CTRL_TYPE_INTEGER64] = ControlAddInteger64,
        [V4L2_CTRL_TYPE_CTRL_CLASS] = ControlAddClass,
        [V4L2_CTRL_TYPE_STRING] = ControlAddString,
        [V4L2_CTRL_TYPE_BITMASK] = ControlAddBitMask,
        [V4L2_CTRL_TYPE_INTEGER_MENU] = ControlAddIntMenu,
    };

    vlc_v4l2_ctrl_t *list = NULL;
    struct v4l2_queryctrl query;

    query.id = V4L2_CTRL_FLAG_NEXT_CTRL;
    while (v4l2_ioctl (fd, VIDIOC_QUERYCTRL, &query) >= 0)
    {
        ctrl_type_cb handler = NULL;
        if (query.type < (sizeof (handlers) / sizeof (handlers[0])))
            handler = handlers[query.type];
        if (handler == NULL)
            handler = ControlAddUnknown;

        vlc_v4l2_ctrl_t *c = handler (obj, fd, &query);
        if (c != NULL)
        {
            vlc_value_t val, text;

            var_AddCallback (obj, c->name, ControlSetCallback, c);
            text.psz_string = (char *)query.name;
            var_Change (obj, c->name, VLC_VAR_SETTEXT, &text, NULL);
            val.i_int = query.id;
            text.psz_string = (char *)c->name;
            var_Change (obj, "controls", VLC_VAR_ADDCHOICE, &val, &text);

            c->next = list;
            list = c;
        }
        query.id |= V4L2_CTRL_FLAG_NEXT_CTRL;
    }

    /* Set well-known controls from VLC configuration */
    for (vlc_v4l2_ctrl_t *ctrl = list; ctrl != NULL; ctrl = ctrl->next)
    {
        if (!CTRL_CID_KNOWN (ctrl->id))
            continue;

        char varname[sizeof (CFG_PREFIX) + sizeof (ctrl->name) - 1];
        sprintf (varname, CFG_PREFIX"%s", ctrl->name);

        int64_t val = var_InheritInteger (obj, varname);
        if (val == -1)
            continue; /* the VLC default value: "do not modify" */
        ControlSet (ctrl, val); /* NOTE: all known are integers or booleans */
    }

    /* Set any control from the VLC configuration control string */
    ControlsSetFromString (obj, list);

    /* Add a control to reset all controls to their default values */
    {
        vlc_value_t val, text;

        var_Create (obj, "reset", VLC_VAR_VOID | VLC_VAR_ISCOMMAND);
        val.psz_string = _("Reset defaults");
        var_Change (obj, "reset", VLC_VAR_SETTEXT, &val, NULL);
        val.i_int = -1;

        text.psz_string = (char *)"reset";
        var_Change (obj, "controls", VLC_VAR_ADDCHOICE, &val, &text);
        var_AddCallback (obj, "reset", ControlsResetCallback, list);
    }
    if (var_InheritBool (obj, CFG_PREFIX"controls-reset"))
        ControlsReset (obj, list);

    return list;
}

void ControlsDeinit (vlc_object_t *obj, vlc_v4l2_ctrl_t *list)
{
    var_DelCallback (obj, "reset", ControlsResetCallback, list);
    var_Destroy (obj, "reset");

    while (list != NULL)
    {
        vlc_v4l2_ctrl_t *next = list->next;

        var_DelCallback (obj, list->name, ControlSetCallback, list);
        var_Destroy (obj, list->name);
        free (list);
        list = next;
    }

    var_Destroy (obj, "controls");
}
