/*
 * Copyright (C) 2017  Red Hat, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, see <http://www.gnu.org/licenses/>.
 *
 * Author: Vratislav Podzimek <vpodzime@redhat.com>
 */

#include <glib.h>
#include <libudev.h>

#include "dev_utils.h"

/**
 * bd_utils_dev_utils_error_quark: (skip)
 */
GQuark bd_utils_dev_utils_error_quark (void)
{
    return g_quark_from_static_string ("g-bd-utils-dev_utils-error-quark");
}

/**
 * bd_utils_resolve_device:
 * @dev_spec: specification of the device (e.g. "/dev/sda", any symlink, or the name of a file
 *            under "/dev")
 * @error: (out) (optional): place to store error (if any)
 *
 * Returns: (transfer full): the full real path of the device (e.g. "/dev/md126"
 *                           for "/dev/md/my_raid") or %NULL in case of error
 */
gchar* bd_utils_resolve_device (const gchar *dev_spec, GError **error) {
    gchar *path = NULL;
    gchar *symlink = NULL;
    GError *l_error = NULL;

    /* TODO: check that the resulting path is a block device? */

    if (!g_str_has_prefix (dev_spec, "/dev/"))
        path = g_strdup_printf ("/dev/%s", dev_spec);
    else
        path = g_strdup (dev_spec);

    symlink = g_file_read_link (path, &l_error);
    if (!symlink) {
        if (g_error_matches (l_error, G_FILE_ERROR, G_FILE_ERROR_INVAL)) {
            /* invalid argument -> not a symlink -> nothing to resolve */
            g_clear_error (&l_error);
            return path;
        } else {
            /* some other error, just report it */
            g_propagate_error (error, l_error);
            g_free (path);
            return NULL;
        }
    }
    g_free (path);

    if (g_str_has_prefix (symlink, "../"))
        path = g_strdup_printf ("/dev/%s", symlink + 3);
    else
        path = g_strdup_printf ("/dev/%s", symlink);
    g_free (symlink);

    return path;
}

/**
 * bd_utils_get_device_symlinks:
 * @dev_spec: specification of the device (e.g. "/dev/sda", any symlink, or the name of a file
 *            under "/dev")
 * @error: (out) (optional): place to store error (if any)
 *
 * Returns: (transfer full) (array zero-terminated=1): a list of all symlinks (known to udev) for the
 *                                                     device specified with @dev_spec or %NULL in
 *                                                     case of error
 */
gchar** bd_utils_get_device_symlinks (const gchar *dev_spec, GError **error) {
    gchar *dev_path;
    struct udev *context;
    struct udev_device *device;
    struct udev_list_entry *entry = NULL;
    struct udev_list_entry *ent_it = NULL;
    guint64 n_links = 0;
    guint64 i = 0;
    gchar **ret = NULL;

    dev_path = bd_utils_resolve_device (dev_spec, error);
    if (!dev_path)
        return NULL;

    context = udev_new ();
    /* dev_path is the full path like "/dev/sda", we only need the device name ("sda") */
    device = udev_device_new_from_subsystem_sysname (context, "block", dev_path + 5);

    if (!device) {
        g_set_error (error, BD_UTILS_DEV_UTILS_ERROR, BD_UTILS_DEV_UTILS_ERROR_FAILED,
                     "Failed to get information about the device '%s' from udev database",
                     dev_path);
        g_free (dev_path);
        udev_unref (context);
        return NULL;
    }

    entry = udev_device_get_devlinks_list_entry (device);
    if (!entry) {
        g_set_error (error, BD_UTILS_DEV_UTILS_ERROR, BD_UTILS_DEV_UTILS_ERROR_FAILED,
                     "Failed to get symlinks for the device '%s'", dev_path);
        g_free (dev_path);
        udev_device_unref (device);
        udev_unref (context);
        return NULL;
    }
    g_free (dev_path);

    ent_it = entry;
    while (ent_it) {
        n_links++;
        ent_it = udev_list_entry_get_next (ent_it);
    }

    ret = g_new0 (gchar*, n_links + 1);
    ent_it = entry;
    while (ent_it) {
        ret[i++] = g_strdup (udev_list_entry_get_name (ent_it));
        ent_it = udev_list_entry_get_next (ent_it);
    }
    ret[i] = NULL;

    udev_device_unref (device);
    udev_unref (context);

    return ret;
}
