/*
 *    HardInfo - Displays System Information
 *    Copyright (C) 2003-2017 Leandro A. F. Pereira <leandro@hardinfo.org>
 *    This file
 *    Copyright (C) 2017 Burt P. <pburt0@gmail.com>
 *    Copyright (C) 2019 Ondrej Čerman
 *
 *    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, version 2.
 *
 *    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, write to the Free Software
 *    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
 */

#include "hardinfo.h"
#include "usb_util.h"
#include "util_ids.h"

#define SYSFS_DIR_USB_DEVICES "/sys/bus/usb/devices"

gchar *usb_ids_file = NULL;

usbi *usbi_new() {
    return g_new0(usbi, 1);
}

void usbi_free(usbi *s) {
    if (s) {
        g_free(s->if_label);
        g_free(s->if_class_str);
        g_free(s->if_subclass_str);
        g_free(s->if_protocol_str);
        g_free(s->driver);
        g_free(s);
    }
}

void usbi_list_free(usbi *s) {
    usbi *n;
    while(s != NULL) {
        n = s->next;
        usbi_free(s);
        s = n;
    }
}

usbd *usbd_new() {
    return g_new0(usbd, 1);
}

void usbd_free(usbd *s) {
    if (s) {
        usbi_list_free(s->if_list);
        vendor_list_free(s->vendors);
        g_free(s->vendor);
        g_free(s->product);
        g_free(s->manufacturer);
        g_free(s->device);
        g_free(s->usb_version);
        g_free(s->device_version);
        g_free(s->serial);
        g_free(s->dev_class_str);
        g_free(s->dev_subclass_str);
        g_free(s->dev_protocol_str);
        g_free(s);
    }
}

void usbd_list_free(usbd *s) {
    usbd *n;
    while(s != NULL) {
        n = s->next;
        usbd_free(s);
        s = n;
    }
}

/* returns number of items after append */
static int usbd_list_append(usbd *l, usbd *n) {
    int c = 0;
    while(l != NULL) {
        c++;
        if (l->next == NULL) {
            if (n != NULL) {
                l->next = n;
                c++;
            }
            break;
        }
        l = l->next;
    }
    return c;
}

/* returns new top of the list */
static usbd *usbd_list_append_sorted(usbd *l, usbd *n) {
    usbd *b = NULL;
    usbd *t = l;

    if (n == NULL)
        return l;

    while(l != NULL) {
        if ((n->bus > l->bus) ||
            ((n->bus == l->bus) && (n->dev >= l->dev))) {
            if (l->next == NULL) {
                l->next = n;
                break;
            }
            b = l;
            l = l->next;
        }
        else {
            if (b == NULL) {
                t = n;
                n->next = l;
            }
            else {
                b->next = n;
                n->next = l;
            }
            break;
        }
    }

    return t;
}

gboolean usbd_append_interface(usbd *dev, usbi *new_if){
    usbi *curr_if;
    if (dev->if_list == NULL){
        dev->if_list = new_if;
        return TRUE;
    }

    if (dev->if_list->if_number == new_if->if_number)
        return FALSE;

    curr_if = dev->if_list;
    while(curr_if->next != NULL) {
        curr_if = curr_if->next;
        if (curr_if->if_number == new_if->if_number)
            return FALSE;
    }
    curr_if->next = new_if;
    return TRUE;
}

int usbd_list_count(usbd *s) {
    return usbd_list_append(s, NULL);
}

static char *lsusb_line_value(char *line, const char *prefix) {
    if (g_str_has_prefix(line, prefix)) {
        line += strlen(prefix) + 1;
        return g_strstrip(line);
    } else
        return NULL;
}

static gboolean usb_get_device_lsusb(int bus, int dev, usbd *s) {
    gboolean spawned;
    gchar *out, *err, *p, *l, *t, *next_nl;
    gchar *lsusb_cmd = g_strdup_printf("lsusb -s %d:%d -v", bus, dev);
    usbi *curr_if = NULL; // do not free

    s->bus = bus;
    s->dev = dev;

    spawned = hardinfo_spawn_command_line_sync(lsusb_cmd,
            &out, &err, NULL, NULL);
    g_free(lsusb_cmd);
    if (spawned) {
        if (strstr(err, "information will be missing")) {
            s->user_scan = TRUE;
        }
        p = out;
        while(next_nl = strchr(p, '\n')) {
            strend(p, '\n');
            g_strstrip(p);

            // device info
            if (l = lsusb_line_value(p, "idVendor")) {
                s->vendor_id = strtol(l, NULL, 0);
                if (t = strchr(l, ' '))
                    s->vendor = g_strdup(t + 1);
            } else if (l = lsusb_line_value(p, "idProduct")) {
                s->product_id = strtol(l, NULL, 0);
                if (t = strchr(l, ' '))
                    s->product = g_strdup(t + 1);
            } else if (l = lsusb_line_value(p, "MaxPower")) {
                s->max_curr_ma = atoi(l);
            } else if (l = lsusb_line_value(p, "bcdUSB")) {
                s->usb_version = g_strdup(l);
            } else if (l = lsusb_line_value(p, "bcdDevice")) {
                s->device_version = g_strdup(l);
            } else if (l = lsusb_line_value(p, "bDeviceClass")) {
                s->dev_class = atoi(l);
                if (t = strchr(l, ' '))
                    s->dev_class_str = g_strdup(t + 1);
            } else if (l = lsusb_line_value(p, "bDeviceSubClass")) {
                s->dev_subclass = atoi(l);
                if (t = strchr(l, ' '))
                    s->dev_subclass_str = g_strdup(t + 1);
            } else if (l = lsusb_line_value(p, "bDeviceProtocol")) {
                s->dev_protocol = atoi(l);
                if (t = strchr(l, ' '))
                    s->dev_protocol_str = g_strdup(t + 1);
            } else if (l = lsusb_line_value(p, "iManufacturer")) {
                if (t = strchr(l, ' '))
                    s->manufacturer = g_strdup(t + 1);
            } else if (l = lsusb_line_value(p, "iProduct")) {
                if (t = strchr(l, ' '))
                    s->device = g_strdup(t + 1);
            } else if (l = lsusb_line_value(p, "iSerial")) {
                if (t = strchr(l, ' '))
                    s->serial = g_strdup(t + 1);

            // interface info
            } else if (l = lsusb_line_value(p, "bInterfaceNumber")) {
                curr_if = usbi_new();
                curr_if->if_number = atoi(l);

                // This may parse same interface multiple times when there
                // are multiple Configuration descriptors per device
                // As a workaround usbd_append_interface appends interface
                // with same if_number only once
                if (!usbd_append_interface(s, curr_if)){
                    usbi_free(curr_if);
                    curr_if = NULL;
                }
            } else if (l = lsusb_line_value(p, "bInterfaceClass")) {
                if (curr_if != NULL){
                    curr_if->if_class = atoi(l);
                    if (t = strchr(l, ' '))
                        curr_if->if_class_str = g_strdup(t + 1);
                }
            } else if (l = lsusb_line_value(p, "bInterfaceSubClass")) {
                if (curr_if != NULL){
                    curr_if->if_subclass = atoi(l);
                    if (t = strchr(l, ' '))
                        curr_if->if_subclass_str = g_strdup(t + 1);
                }
            } else if (l = lsusb_line_value(p, "bInterfaceProtocol")) {
                if (curr_if != NULL){
                    curr_if->if_protocol = atoi(l);
                    if (t = strchr(l, ' '))
                        curr_if->if_protocol_str = g_strdup(t + 1);
                }
            } else if (l = lsusb_line_value(p, "iInterface")) {
                if (curr_if != NULL){
                    if (t = strchr(l, ' '))
                        curr_if->if_label = g_strdup(t + 1);
                }
            }

            p = next_nl + 1;
        }
        g_free(out);
        g_free(err);
        return TRUE;
    }
    return FALSE;
}

static gboolean usb_get_interface_sysfs(int conf, int number,
                                        const char* devpath, usbi *intf){
    gchar *ifpath, *drvpath, *tmp;
    ids_query_result result = {};

    ifpath = g_strdup_printf("%s:%d.%d", devpath, conf, number);
    if (!g_file_test(ifpath, G_FILE_TEST_EXISTS)){
        return FALSE;
    }

    tmp = g_strdup_printf("%s/driver", ifpath);
    drvpath = g_file_read_link(tmp, NULL);
    g_free(tmp);
    if (drvpath){
        intf->driver = g_path_get_basename(drvpath);
        g_free(drvpath);
    }
    else{
        intf->driver = g_strdup(_("(None)"));
    }

    intf->if_number = number;
    intf->if_class = h_sysfs_read_hex(ifpath, "bInterfaceClass");
    intf->if_subclass = h_sysfs_read_hex(ifpath, "bInterfaceSubClass");
    intf->if_protocol = h_sysfs_read_hex(ifpath, "bInterfaceProtocol");

    if (intf->if_label == NULL)
        intf->if_label = h_sysfs_read_string(ifpath, "interface");

    if (intf->if_class_str == NULL && intf->if_subclass_str == NULL
                                   && intf->if_protocol_str == NULL) {
        tmp = g_strdup_printf("C %02x/%02x/%02x", intf->if_class,
                               intf->if_subclass, intf->if_protocol);
        scan_ids_file(usb_ids_file, tmp, &result, -1);
        if (result.results[0])
            intf->if_class_str = g_strdup(result.results[0]);
        if (result.results[1])
            intf->if_subclass_str = g_strdup(result.results[1]);
        if (result.results[2])
            intf->if_protocol_str = g_strdup(result.results[2]);
        g_free(tmp);
    }

    g_free(ifpath);
    return TRUE;
}

static void find_usb_ids_file() {
    if (usb_ids_file) return;
    char *file_search_order[] = {
        g_build_filename(g_get_user_config_dir(), "hardinfo", "usb.ids", NULL),
        g_build_filename(params.path_data, "usb.ids", NULL),
        NULL
    };
    int n;
    for(n = 0; file_search_order[n]; n++) {
        if (!usb_ids_file && !access(file_search_order[n], R_OK))
            usb_ids_file = file_search_order[n];
        else
            g_free(file_search_order[n]);
    }
}

void usb_lookup_ids_vendor_product_str(gint vendor_id, gint product_id,
                                       gchar **vendor_str, gchar **product_str) {
    ids_query_result result = {};
    gchar *qpath;

    if (!usb_ids_file)
        find_usb_ids_file();
    if (!usb_ids_file)
        return;

    qpath = g_strdup_printf("%04x/%04x", vendor_id, product_id);
    scan_ids_file(usb_ids_file, qpath, &result, -1);
    if (result.results[0])
        *vendor_str = g_strdup(result.results[0]);
    if (result.results[1])
        *product_str = g_strdup(result.results[1]);
    g_free(qpath);
}

static gboolean usb_get_device_sysfs(int bus, int dev, const char* sysfspath, usbd *s) {
    usbi *intf;
    gboolean ok;
    int i, if_count = 0, conf = 0, ver;
    ids_query_result result = {};
    gchar *qpath;

    if (sysfspath == NULL)
        return FALSE;

    if (!usb_ids_file)
        find_usb_ids_file();

    s->bus = bus;
    s->dev = dev;
    s->dev_class = h_sysfs_read_hex(sysfspath, "bDeviceClass");
    s->vendor_id = h_sysfs_read_hex(sysfspath, "idVendor");
    s->product_id = h_sysfs_read_hex(sysfspath, "idProduct");
    s->manufacturer = h_sysfs_read_string(sysfspath, "manufacturer");
    s->device = h_sysfs_read_string(sysfspath, "product");
    s->max_curr_ma = h_sysfs_read_int(sysfspath, "bMaxPower");
    s->dev_class = h_sysfs_read_hex(sysfspath, "bDeviceClass");
    s->dev_subclass = h_sysfs_read_hex(sysfspath, "bDeviceSubClass");
    s->dev_protocol = h_sysfs_read_hex(sysfspath, "bDeviceProtocol");
    s->speed_mbs = h_sysfs_read_int(sysfspath, "speed");

    if (s->product == NULL && s->vendor == NULL) {
        qpath = g_strdup_printf("%04x/%04x", s->vendor_id, s->product_id);
        scan_ids_file(usb_ids_file, qpath, &result, -1);
        if (result.results[0])
            s->vendor = g_strdup(result.results[0]);
        if (result.results[1])
            s->product = g_strdup(result.results[1]);
        g_free(qpath);
    }

    if (s->dev_class_str == NULL && s->dev_subclass_str == NULL
                                 && s->dev_protocol_str == NULL) {
        qpath = g_strdup_printf("C %02x/%02x/%02x", s->dev_class,
                                   s->dev_subclass, s->dev_protocol);
        scan_ids_file(usb_ids_file, qpath, &result, -1);
        if (result.results[0])
            s->dev_class_str = g_strdup(result.results[0]);
        if (result.results[1])
            s->dev_subclass_str = g_strdup(result.results[1]);
        if (result.results[2])
            s->dev_protocol_str = g_strdup(result.results[2]);
        g_free(qpath);
    }

    conf = h_sysfs_read_hex(sysfspath, "bConfigurationValue");

    if (s->usb_version == NULL)
        s->usb_version = h_sysfs_read_string(sysfspath, "version");

    if (s->serial == NULL)
        s->serial = h_sysfs_read_string(sysfspath, "serial");

    if (s->device_version == NULL) {
        ver = h_sysfs_read_int(sysfspath, "bcdDevice");
        if (ver > 0){
            s->device_version = g_strdup_printf("%d.%02d", ver/100, ver%100);
        }
    }

    if (s->if_list == NULL){ // create interfaces list
        if_count = h_sysfs_read_int(sysfspath, "bNumInterfaces");
        for (i = 0; i <= if_count; i++){
            intf = usbi_new();
            ok = usb_get_interface_sysfs(conf, i, sysfspath, intf);
            if (ok){
                if (!usbd_append_interface(s, intf)){
                    usbi_free(intf);
                }
            }
            else{
                usbi_free(intf);
            }
        }
    }
    else{ // improve existing list
        intf = s->if_list;
        while (intf){
            usb_get_interface_sysfs(conf, intf->if_number, sysfspath, intf);
            intf = intf->next;
        }
    }

    return TRUE;
}

usbd *usb_get_device(int bus, int dev, const gchar* sysfspath) {
    usbd *s = usbd_new();
    int ok = 0;
    if (s) {
        /* try sysfs */
        ok = usb_get_device_sysfs(bus, dev, sysfspath, s);
        if (!ok) {
            /* try lsusb */
            ok = usb_get_device_lsusb(bus, dev, s);
        }
        if (!ok) {
            usbd_free(s);
            s = NULL;
        }
    }
    return s;
}

static usbd *usb_get_device_list_lsusb() {
    gboolean spawned;
    gchar *out, *err, *p, *next_nl;
    usbd *head = NULL, *nd;
    int bus, dev, vend, prod, ec;

    spawned = hardinfo_spawn_command_line_sync("lsusb",
            &out, &err, NULL, NULL);
    if (spawned) {
        p = out;
        while(next_nl = strchr(p, '\n')) {
            strend(p, '\n');
            ec = sscanf(p, "Bus %d Device %d: ID %x:%x", &bus, &dev, &vend, &prod);
            if (ec == 4) {
                nd = usb_get_device(bus, dev, NULL);
                if (head == NULL) {
                    head = nd;
                } else {
                    usbd_list_append(head, nd);
                }
            }
            p = next_nl + 1;
        }
        g_free(out);
        g_free(err);
    }
    return head;
}

static usbd *usb_get_device_list_sysfs() {
    GDir *dir;
    GRegex *regex;
    GMatchInfo *match_info;
    usbd *head = NULL, *nd;
    gchar *devpath;
    const char *entry;
    int bus, dev;

    regex = g_regex_new("^([0-9]+-[0-9]+([.][0-9]+)*)|(usb[0-9]+)$", 0, 0, NULL);
    if (!regex){
        return NULL;
    }

    dir = g_dir_open(SYSFS_DIR_USB_DEVICES, 0, NULL);
    if (!dir){
        return NULL;
    }

    while ((entry = g_dir_read_name(dir))) {
        g_regex_match(regex, entry, 0, &match_info);

        if (g_match_info_matches(match_info)) {
            devpath = g_build_filename(SYSFS_DIR_USB_DEVICES, entry, NULL);
            bus = h_sysfs_read_int(devpath, "busnum");
            dev = h_sysfs_read_int(devpath, "devnum");

            if (bus > 0 && dev > 0){
                nd = usb_get_device(bus, dev, devpath);
                if (head == NULL) {
                    head = nd;
                } else {
                    head = usbd_list_append_sorted(head, nd);
                }
            }
            g_free(devpath);
        }
        g_match_info_free(match_info);
    }

    g_dir_close(dir);
    g_regex_unref(regex);

    return head;
}

usbd *usb_get_device_list() {
    usbd *lst, *l;

    lst = usb_get_device_list_sysfs();
    if (lst == NULL)
        lst = usb_get_device_list_lsusb();

    l = lst;
    while(l) {
        l->vendors = vendor_list_remove_duplicates_deep(vendors_match(l->vendor, l->manufacturer, NULL));
        l = l->next;
    }

    return lst;
}