/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * qmi-firmware-update -- Command line tool to update firmware in QMI devices
 *
 * 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Copyright (C) 2022 VMware, Inc.
 */

#include "config.h"
#include <stdlib.h>
#include <gio/gio.h>

#if defined WITH_UDEV
# error udev found
#endif

#include "qfu-helpers.h"
#include "qfu-helpers-sysfs.h"

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

static const gchar *tty_subsys_list[]     = { "tty", NULL };
static const gchar *cdc_wdm_subsys_list[] = { "usbmisc", "usb", NULL };

static gboolean
has_sysfs_attribute (const gchar *path,
                     const gchar *attribute)
{
    g_autofree gchar *aux_filepath = NULL;

    aux_filepath = g_strdup_printf ("%s/%s", path, attribute);
    return g_file_test (aux_filepath, G_FILE_TEST_EXISTS);
}

static gchar *
read_sysfs_attribute_as_string (const gchar *path,
                                const gchar *attribute)
{
    g_autofree gchar *aux = NULL;
    gchar            *contents = NULL;

    aux = g_strdup_printf ("%s/%s", path, attribute);
    if (g_file_get_contents (aux, &contents, NULL, NULL)) {
        g_strdelimit (contents, "\r\n", ' ');
        g_strstrip (contents);
    }
    return contents;
}

static gulong
read_sysfs_attribute_as_num (const gchar *path,
                             const gchar *attribute,
                             guint        base)
{
    g_autofree gchar *contents = NULL;
    gulong            val = 0;

    contents = read_sysfs_attribute_as_string (path, attribute);
    if (contents)
        val = strtoul (contents, NULL, base);
    return val;
}

static gchar *
read_sysfs_attribute_link_basename (const gchar *path,
                                    const gchar *attribute)
{
    g_autofree gchar *aux_filepath = NULL;
    g_autofree gchar *canonicalized_path = NULL;

    aux_filepath = g_strdup_printf ("%s/%s", path, attribute);
    if (!g_file_test (aux_filepath, G_FILE_TEST_EXISTS))
        return NULL;

    canonicalized_path = realpath (aux_filepath, NULL);
    return g_path_get_basename (canonicalized_path);
}

static gboolean
get_device_details (const gchar           *port_sysfs_path,
                    gchar                **out_sysfs_path,
                    QfuHelpersDeviceMode  *out_device_mode,
                    guint16               *out_vid,
                    guint16               *out_pid,
                    guint                 *out_busnum,
                    guint                 *out_devnum,
                    GError               **error)
{
    g_autofree gchar *iter = NULL;
    g_autofree gchar *physdev_sysfs_path = NULL;
    gulong            aux;

    if (out_vid)
        *out_vid = 0;
    if (out_pid)
        *out_pid = 0;
    if (out_busnum)
        *out_busnum = 0;
    if (out_devnum)
        *out_devnum = 0;
    if (out_sysfs_path)
        *out_sysfs_path = NULL;
    if (out_device_mode)
        *out_device_mode = QFU_HELPERS_DEVICE_MODE_UNKNOWN;

    iter = realpath (port_sysfs_path, NULL);

    while (iter && (g_strcmp0 (iter, "/") != 0)) {
        gchar *parent;

        /* is this the USB physdev? */
        if (has_sysfs_attribute (iter, "idVendor")) {
            /* stop traversing as soon as the physical device is found */
            physdev_sysfs_path = g_steal_pointer (&iter);
            break;
        }

        parent = g_path_get_dirname (iter);
        g_clear_pointer (&iter, g_free);
        iter = parent;
    }

    if (!physdev_sysfs_path) {
        g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "couldn't find parent physical USB device");
        return FALSE;
    }

    if (out_device_mode) {
        aux = read_sysfs_attribute_as_num (physdev_sysfs_path, "bNumInterfaces", 10);
        /* QDL download mode has exactly one single USB interface */
        if (aux == 1)
            *out_device_mode = QFU_HELPERS_DEVICE_MODE_DOWNLOAD;
        else if (aux > 1)
            *out_device_mode = QFU_HELPERS_DEVICE_MODE_MODEM;
    }

    if (out_vid) {
        aux = read_sysfs_attribute_as_num (physdev_sysfs_path, "idVendor", 16);
        if (aux <= G_MAXUINT16)
            *out_vid = (guint16) aux;
    }

    if (out_pid) {
        aux = read_sysfs_attribute_as_num (physdev_sysfs_path, "idProduct", 16);
        if (aux <= G_MAXUINT16)
            *out_pid = (guint16) aux;
    }

    if (out_busnum) {
        aux = read_sysfs_attribute_as_num (physdev_sysfs_path, "busnum", 10);
        if (aux <= G_MAXUINT)
            *out_busnum = (guint16) aux;
    }

    if (out_devnum) {
        aux = read_sysfs_attribute_as_num (physdev_sysfs_path, "devnum", 10);
        if (aux <= G_MAXUINT)
            *out_devnum = (guint16) aux;
    }

    if (out_sysfs_path)
        *out_sysfs_path = g_steal_pointer (&physdev_sysfs_path);

    return TRUE;
}

static gboolean
get_interface_details (const gchar  *port_sysfs_path,
                       gchar       **out_driver,
                       GError      **error)
{
    g_autofree gchar *iter = NULL;
    g_autofree gchar *interface_sysfs_path = NULL;

    if (out_driver)
        *out_driver = NULL;

    iter = realpath (port_sysfs_path, NULL);

    while (iter && (g_strcmp0 (iter, "/") != 0)) {
        gchar *parent;

        /* is this the USB interface? */
        if (has_sysfs_attribute (iter, "bInterfaceClass")) {
            /* stop traversing as soon as the physical device is found */
            interface_sysfs_path = g_steal_pointer (&iter);
            break;
        }

        parent = g_path_get_dirname (iter);
        g_clear_pointer (&iter, g_free);
        iter = parent;
    }

    if (!interface_sysfs_path) {
        g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "couldn't find parent interface USB device");
        return FALSE;
    }

    if (out_driver)
        *out_driver = read_sysfs_attribute_link_basename (interface_sysfs_path, "driver");

    return TRUE;
}

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

gchar *
qfu_helpers_sysfs_find_by_file (GFile   *file,
                                GError **error)
{
    const gchar      **subsys_list = NULL;
    g_autofree gchar  *physdev_sysfs_path = NULL;
    g_autofree gchar  *found_port_sysfs_path = NULL;
    g_autofree gchar  *basename = NULL;
    guint             i;

    basename = g_file_get_basename (file);
    if (!basename) {
        g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "couldn't get filename");
        return NULL;
    }

    if (g_str_has_prefix (basename, "tty"))
        subsys_list = tty_subsys_list;
    else if  (g_str_has_prefix (basename, "cdc-wdm"))
        subsys_list = cdc_wdm_subsys_list;
    else {
        g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "unknown device file type");
        return NULL;
    }

    for (i = 0; !found_port_sysfs_path && subsys_list[i]; i++) {
        g_autofree gchar *tmp = NULL;
        g_autofree gchar *port_sysfs_path = NULL;

        tmp = g_strdup_printf ("/sys/class/%s/%s", subsys_list[i], basename);

        port_sysfs_path = realpath (tmp, NULL);
        if (port_sysfs_path && g_file_test (port_sysfs_path, G_FILE_TEST_EXISTS))
            found_port_sysfs_path = g_steal_pointer (&port_sysfs_path);
    }

    if (!found_port_sysfs_path) {
        g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "device not found");
        return NULL;
    }

    if (!get_device_details (found_port_sysfs_path,
                             &physdev_sysfs_path, NULL, NULL, NULL, NULL, NULL,
                             error))
        return NULL;

    g_debug ("[qfu-sysfs] sysfs path for '%s' found: %s", basename, physdev_sysfs_path);
    return g_steal_pointer (&physdev_sysfs_path);
}

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

static gboolean
device_already_added (GPtrArray   *ptr,
                      const gchar *sysfs_path)
{
    guint i;

    for (i = 0; i < ptr->len; i++) {
        if (g_strcmp0 (g_ptr_array_index (ptr, i), sysfs_path) == 0)
            return TRUE;
    }

    return FALSE;
}

static GPtrArray *
find_by_device_info_in_subsystem (GPtrArray             *sysfs_paths,
                                  const gchar           *subsystem,
                                  guint16                vid,
                                  guint16                pid,
                                  guint                  busnum,
                                  guint                  devnum)
{
    g_autofree gchar           *subsys_sysfs_path = NULL;
    g_autoptr(GFile)            subsys_sysfs_file = NULL;
    g_autoptr(GFileEnumerator)  direnum = NULL;

    subsys_sysfs_path = g_strdup_printf ("/sys/class/%s", subsystem);
    subsys_sysfs_file = g_file_new_for_path (subsys_sysfs_path);
    direnum = g_file_enumerate_children (subsys_sysfs_file,
                                         G_FILE_ATTRIBUTE_STANDARD_NAME,
                                         G_FILE_QUERY_INFO_NONE,
                                         NULL,
                                         NULL);
    if (direnum) {
        while (TRUE) {
            GFileInfo            *info = NULL;
            g_autoptr(GFile)      child = NULL;
            g_autofree gchar     *child_path = NULL;
            guint16               device_vid = 0;
            guint16               device_pid = 0;
            guint                 device_busnum = 0;
            guint                 device_devnum = 0;
            g_autofree gchar     *device_sysfs_path = NULL;

            if (!g_file_enumerator_iterate (direnum, &info, NULL, NULL, NULL) || !info)
                break;

            child = g_file_enumerator_get_child (direnum, info);
            child_path = g_file_get_path (child);
            if (get_device_details (child_path,
                                    &device_sysfs_path,
                                    NULL,
                                    &device_vid,
                                    &device_pid,
                                    &device_busnum,
                                    &device_devnum,
                                    NULL)) {
                if ((vid == 0 || vid == device_vid) &&
                    (pid == 0 || pid == device_pid) &&
                    (busnum == 0 || busnum == device_busnum) &&
                    (devnum == 0 || devnum == device_devnum) &&
                    (!device_already_added (sysfs_paths, device_sysfs_path)))
                    g_ptr_array_add (sysfs_paths, g_steal_pointer (&device_sysfs_path));
            }
        }
    }

    return sysfs_paths;
}

gchar *
qfu_helpers_sysfs_find_by_device_info (guint16   vid,
                                       guint16   pid,
                                       guint     busnum,
                                       guint     devnum,
                                       GError  **error)
{
    g_autoptr(GPtrArray) sysfs_paths = NULL;
    g_autoptr(GString)   match_str = NULL;
    guint                i;

    sysfs_paths = g_ptr_array_new_with_free_func (g_free);

    match_str   = g_string_new ("");
    if (vid != 0)
        g_string_append_printf (match_str, "vid 0x%04x", vid);
    if (pid != 0)
        g_string_append_printf (match_str, "%spid 0x%04x", match_str->len > 0 ? ", " : "", pid);
    if (busnum != 0)
        g_string_append_printf (match_str, "%sbus %03u", match_str->len > 0 ? ", " : "", busnum);
    if (devnum != 0)
        g_string_append_printf (match_str, "%sdev %03u", match_str->len > 0 ? ", " : "", devnum);
    g_assert (match_str->len > 0);

    for (i = 0; tty_subsys_list[i]; i++)
        sysfs_paths = find_by_device_info_in_subsystem (sysfs_paths,
                                                        tty_subsys_list[i],
                                                        vid, pid, busnum, devnum);

    for (i = 0; cdc_wdm_subsys_list[i]; i++)
        sysfs_paths = find_by_device_info_in_subsystem (sysfs_paths,
                                                        cdc_wdm_subsys_list[i],
                                                        vid, pid, busnum, devnum);

    for (i = 0; i < sysfs_paths->len; i++)
        g_debug ("[%s] sysfs path: %s", match_str->str, (gchar *) g_ptr_array_index (sysfs_paths, i));

    if (sysfs_paths->len == 0) {
        g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                     "no device found with matching criteria: %s",
                     match_str->str);
        return NULL;
    }

    if (sysfs_paths->len > 1) {
        g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                     "multiple devices (%u) found with matching criteria: %s",
                     sysfs_paths->len, match_str->str);
        return NULL;
    }

    return g_strdup (g_ptr_array_index (sysfs_paths, 0));
}

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

static GFile *
device_matches (GFile                *file,
                QfuHelpersDeviceType  type,
                QfuHelpersDeviceMode  mode,
                const gchar          *sysfs_path)
{
    g_autofree gchar     *port_sysfs_path = NULL;
    g_autofree gchar     *device_sysfs_path = NULL;
    g_autofree gchar     *device_driver = NULL;
    g_autofree gchar     *device_path = NULL;
    QfuHelpersDeviceMode  device_mode = QFU_HELPERS_DEVICE_MODE_UNKNOWN;

    port_sysfs_path = g_file_get_path (file);
    if (!get_device_details (port_sysfs_path,
                             &device_sysfs_path, &device_mode, NULL, NULL, NULL, NULL,
                             NULL))
        return NULL;

    if (!device_sysfs_path)
        return NULL;

    if (g_strcmp0 (device_sysfs_path, sysfs_path) != 0)
        return NULL;

    if (device_mode != mode)
        return NULL;

    if (!get_interface_details (port_sysfs_path, &device_driver, NULL))
        return NULL;

    switch (type) {
    case QFU_HELPERS_DEVICE_TYPE_TTY:
        if (g_strcmp0 (device_driver, "qcserial") != 0)
            return NULL;
        break;
    case QFU_HELPERS_DEVICE_TYPE_CDC_WDM:
        if (g_strcmp0 (device_driver, "qmi_wwan") != 0 && g_strcmp0 (device_driver, "cdc_mbim") != 0)
            return NULL;
        break;
    case QFU_HELPERS_DEVICE_TYPE_LAST:
    default:
        g_assert_not_reached ();
    }

    device_path = g_strdup_printf ("/dev/%s", g_file_get_basename (file));
    return g_file_new_for_path (device_path);
}

GList *
qfu_helpers_sysfs_list_devices (QfuHelpersDeviceType  device_type,
                                QfuHelpersDeviceMode  device_mode,
                                const gchar          *sysfs_path)
{
    const gchar **subsys_list = NULL;
    guint         i;
    GList        *files = NULL;

    switch (device_type) {
    case QFU_HELPERS_DEVICE_TYPE_TTY:
        subsys_list = tty_subsys_list;
        break;
    case QFU_HELPERS_DEVICE_TYPE_CDC_WDM:
        subsys_list = cdc_wdm_subsys_list;
        break;
    case QFU_HELPERS_DEVICE_TYPE_LAST:
    default:
        g_assert_not_reached ();
    }

    for (i = 0; subsys_list[i]; i++) {
        g_autofree gchar           *subsys_sysfs_path = NULL;
        g_autoptr(GFile)            subsys_sysfs_file = NULL;
        g_autoptr(GFileEnumerator)  direnum = NULL;

        subsys_sysfs_path = g_strdup_printf ("/sys/class/%s", subsys_list[i]);
        subsys_sysfs_file = g_file_new_for_path (subsys_sysfs_path);
        direnum = g_file_enumerate_children (subsys_sysfs_file,
                                             G_FILE_ATTRIBUTE_STANDARD_NAME,
                                             G_FILE_QUERY_INFO_NONE,
                                             NULL,
                                             NULL);
        if (!direnum)
            continue;

        while (TRUE) {
            GFileInfo        *info = NULL;
            g_autoptr(GFile)  child = NULL;
            g_autoptr(GFile)  devfile = NULL;

            if (!g_file_enumerator_iterate (direnum, &info, NULL, NULL, NULL) || !info)
                break;

            child = g_file_enumerator_get_child (direnum, info);
            devfile = device_matches (child, device_type, device_mode, sysfs_path);
            if (devfile)
                files = g_list_prepend (files, g_steal_pointer (&devfile));
        }
    }

    return files;
}

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

/* Check for the new port addition every 10s */
#define WAIT_FOR_DEVICE_CHECK_SECS 10

/* And up to 12 attempts to check (so 120s in total) */
#define WAIT_FOR_DEVICE_CHECK_ATTEMPTS 12

typedef struct {
    QfuHelpersDeviceType  device_type;
    QfuHelpersDeviceMode  device_mode;
    gchar                *sysfs_path;
    gchar                *peer_port;
    guint                 check_attempts;
    guint                 timeout_id;
    gulong                cancellable_id;
} WaitForDeviceContext;

static void
wait_for_device_context_free (WaitForDeviceContext *ctx)
{
    g_assert (!ctx->timeout_id);
    g_assert (!ctx->cancellable_id);

    g_free (ctx->sysfs_path);
    g_free (ctx->peer_port);
    g_slice_free (WaitForDeviceContext, ctx);
}

GFile *
qfu_helpers_sysfs_wait_for_device_finish (GAsyncResult  *res,
                                          GError       **error)
{
    return G_FILE (g_task_propagate_pointer (G_TASK (res), error));
}

static GFile *
wait_for_device_lookup (QfuHelpersDeviceType  device_type,
                        QfuHelpersDeviceMode  device_mode,
                        const gchar          *sysfs_path,
                        const gchar          *peer_port)
{
    GList *devices;
    GFile *file;

    devices = qfu_helpers_sysfs_list_devices (device_type, device_mode, sysfs_path);
    if (!devices) {
        g_autofree gchar *tmp = NULL;
        g_autofree gchar *path = NULL;

        if (!peer_port)
            return NULL;

        /* Check with peer port */
        tmp = g_build_filename (peer_port, "device", NULL);
        path = realpath (tmp, NULL);
        if (path) {
            g_debug ("[qfu-sysfs] peer lookup: %s => %s", peer_port, path);
            devices = qfu_helpers_sysfs_list_devices (device_type, device_mode, path);
            if (!devices)
                return NULL;
        }
    }

    if (g_list_length (devices) > 1)
        g_warning ("[qfu-sysfs] waiting device (%s) matched multiple times",
                   qfu_helpers_device_type_to_string (device_type));

    /* Take the first one from the list */
    file = G_FILE (g_object_ref (devices->data));
    g_list_free_full (devices, g_object_unref);

    return file;
}

static gboolean
wait_for_device_check (GTask *task)
{
    WaitForDeviceContext *ctx;
    g_autoptr(GFile)      device = NULL;
    g_autofree gchar     *device_name = NULL;

    ctx = (WaitForDeviceContext *) g_task_get_task_data (task);

    /* Check devices matching */
    device = wait_for_device_lookup (ctx->device_type, ctx->device_mode, ctx->sysfs_path, ctx->peer_port);
    if (!device) {
        /* No devices found */
        if (ctx->check_attempts == WAIT_FOR_DEVICE_CHECK_ATTEMPTS) {
            /* Disconnect this handler */
            ctx->timeout_id = 0;

            /* Disconnect the other handlers */
            g_cancellable_disconnect (g_task_get_cancellable (task), ctx->cancellable_id);
            ctx->cancellable_id = 0;

            g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_TIMED_OUT,
                                     "waiting for device at '%s' timed out",
                                     ctx->sysfs_path);
            g_object_unref (task);
            return G_SOURCE_REMOVE;
        }

        /* go on with next attempt */
        ctx->check_attempts++;
        return G_SOURCE_CONTINUE;
    }

    device_name = g_file_get_basename (device);
    g_debug ("[qfu-sysfs] waiting device (%s) matched: %s",
             qfu_helpers_device_type_to_string (ctx->device_type),
             device_name);

    /* Disconnect the other handlers */
    g_cancellable_disconnect (g_task_get_cancellable (task), ctx->cancellable_id);
    ctx->cancellable_id = 0;

    /* Disconnect this handler */
    ctx->timeout_id = 0;

    g_task_return_pointer (task, g_steal_pointer (&device), g_object_unref);
    g_object_unref (task);

    return G_SOURCE_REMOVE;
}

static void
wait_for_device_cancelled (GCancellable *cancellable,
                           GTask        *task)
{
    WaitForDeviceContext *ctx;

    ctx = (WaitForDeviceContext *) g_task_get_task_data (task);

    /* Disconnect this handler */
    g_cancellable_disconnect (g_task_get_cancellable (task), ctx->cancellable_id);
    ctx->cancellable_id = 0;

    /* Disconnect the other handlers */
    g_source_remove (ctx->timeout_id);
    ctx->timeout_id = 0;

    g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_CANCELLED,
                             "waiting for device at '%s' cancelled",
                             ctx->sysfs_path);
    g_object_unref (task);
}

void
qfu_helpers_sysfs_wait_for_device (QfuHelpersDeviceType  device_type,
                                   QfuHelpersDeviceMode  device_mode,
                                   const gchar          *sysfs_path,
                                   const gchar          *peer_port,
                                   GCancellable         *cancellable,
                                   GAsyncReadyCallback   callback,
                                   gpointer              user_data)
{
    GTask                *task;
    WaitForDeviceContext *ctx;

    ctx = g_slice_new0 (WaitForDeviceContext);
    ctx->device_type = device_type;
    ctx->device_mode = device_mode;
    ctx->sysfs_path = g_strdup (sysfs_path);
    ctx->peer_port = g_strdup (peer_port);

    task = g_task_new (NULL, cancellable, callback, user_data);
    g_task_set_task_data (task, ctx, (GDestroyNotify) wait_for_device_context_free);

    /* Allow cancellation */
    ctx->cancellable_id = g_cancellable_connect (cancellable,
                                                 (GCallback) wait_for_device_cancelled,
                                                 task,
                                                 NULL);

    /* Schedule lookup of of port every once in a while */
    ctx->timeout_id = g_timeout_add_seconds (WAIT_FOR_DEVICE_CHECK_SECS,
                                             (GSourceFunc) wait_for_device_check,
                                             task);

    /* Note: task ownership is shared between the signals and the timeout */
}
