/* discover.c -- Hardware-querying utility
 *
 * $Progeny$
 *
 * Copyright 2000, 2001, 2002 Progeny Linux Systems, Inc.
 * Copyright 2002 Hewlett-Packard Company
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 * THE COPYRIGHT HOLDER(S) BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */

#include "config.h"

#include <assert.h>
#include <err.h>
#include <getopt.h>
#include <stdio.h>
#include <sysexits.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>

#include <discover/discover.h>
#include <discover/discover-conf.h>
#include <discover/discover-xml.h>

#include <discover/device.h>

enum action {
    BUS_SUMMARY,
    QUERY_DATA,
    TYPE_SUMMARY
};

#define FORMAT_LENGTH 20

typedef struct format_details {
    int error_char;  /* -1 means no error */
    int plain_output;  /* -1 means no substitution, use format string */
    char format_substring[FORMAT_LENGTH+1];
} format_details_t;

typedef struct linked_list_node {
    char *buffer;
    struct linked_list_node *next;
} linked_list_node_t;

typedef struct linked_list {
    linked_list_node_t *head;
    linked_list_node_t *cur;
} linked_list_t;

static int do_model = 1;
static int do_vendor = 1;
static int do_model_id = 0;
static int do_vendor_id = 0;
static int verbose = 0;
static int normalize_data = 0;
static char *format_string = NULL;
static char shortopts[] = "bd:e:htVv";
static linked_list_t *path_list;
static linked_list_t *data_list;

/* I use 1 in the fourth field for all long options which have no
 * equivalent short options, then (in the while loop) choose how to
 * react based on the first field.  I hate getopt_long.
*/
static struct option longopts[] = {
    /* actions */
    { "bus-summary",     no_argument,       NULL, 'b' }, /* Default */
    { "data-path",       required_argument, NULL, 1 },
    { "help",            no_argument,       NULL, 'h' },
    { "type-summary",    no_argument,       NULL, 't' },
    { "version",         no_argument,       NULL, 'V' },

    /* options */
    { "disable-bus",     required_argument, NULL, 'd' },
    { "enable-bus",      required_argument, NULL, 'e' },
    { "data-version",    required_argument, NULL, 1 },
    { "insert-url",      required_argument, NULL, 1 },
    { "append-url",      required_argument, NULL, 1 },
    { "format",          required_argument, NULL, 1 },
    { "normalize-whitespace", no_argument,  NULL, 1 },
    { "model",           no_argument,       &do_model, 1 }, /* Default */
    { "model-id",        no_argument,       &do_model_id, 1 },
    { "no-model",        no_argument,       &do_model, 0 },
    { "no-model-id",     no_argument,       &do_model_id, 0 }, /* Default */
    { "no-vendor",       no_argument,       &do_vendor, 0 },
    { "no-vendor-id",    no_argument,       &do_vendor_id, 0 }, /* Default */
    { "vendor",          no_argument,       &do_vendor, 1 }, /* Default */
    { "vendor-id",       no_argument,       &do_vendor_id, 1 },
    { "verbose",         no_argument,       NULL, 'v' },
    { 0, 0, 0, 0 }
};

/******************************************************************************
 * Ye Olde Debugging Hacks
 */

#ifdef MEMORY_DEBUG

static void
memdebug_free(discover_bus_map_t *busmap)
{
    busmap->free_devices();
    busmap->xml_free_busclasses();
    busmap->xml_free_devices();
    busmap->xml_free_vendors();
    busmap->xml_free_busclass_urls();
    busmap->xml_free_device_urls();
    busmap->xml_free_vendor_urls();

    discover_xml_free_urls();
    discover_conf_free();
}

#else

#define memdebug_free(busmap)

#endif

/******************************************************************************
 * Helpers
 */

static char *
ll_next(linked_list_t *list)
{
    linked_list_node_t *cur_node;

    assert(list != NULL);

    if (!list->cur) {
        return NULL;
    }

    cur_node = list->cur;

    list->cur = list->cur->next;

    return cur_node->buffer;
}

static void
ll_add(linked_list_t *list, char *text)
{
    linked_list_node_t *new_node, *cur_node;

    assert(list != NULL);

    new_node = calloc(1, sizeof(linked_list_node_t));
    new_node->buffer = strdup(text);

    if (!list->head) {
        list->head = new_node;
        list->cur = list->head;
    } else {
        cur_node = list->head;
        while(cur_node->next) {
            cur_node = cur_node->next;
        }
        cur_node->next = new_node;
    }
}

static char *
ll_last(linked_list_t *list)
{
    linked_list_node_t *cur_node;

    assert(list != NULL);

    if (!list->head) {
        return NULL;
    }

    cur_node = list->head;

    while(cur_node->next) {
        cur_node = cur_node->next;
    }

    return cur_node->buffer;
}

static linked_list_t *
ll_new(void)
{
    return calloc(1, sizeof(linked_list_t));
}

static void
ll_node_free(linked_list_node_t *node)
{
    if (node != NULL) {
        assert(node->buffer != NULL);

        ll_node_free(node->next);
        free(node->buffer);
        free(node);
    }
}

static void
ll_free(linked_list_t *list)
{
    assert(list != NULL);

    if (list->head) {
        ll_node_free(list->head);
    }
    free(list);
}

static void
ll_rewind(linked_list_t *list)
{
    assert(list != NULL);

    if (list->head) {
        list->cur = list->head;
    }
}

static void
print_version(void)
{
    puts(PACKAGE_STRING);
}

static void
print_usage(void)
{
    puts("\
usage: discover [--bus-summary] [OPTIONS] [BUS [...]]\n\
       discover --type-summary [OPTIONS] [TYPE [...]]\n\
       discover --data-path=PATH [--data-version=VERSION] [TYPE | ID] [...]\n\
       discover --version\n\
       discover --help");
}

static void
print_help(void)
{
    print_usage();
    puts("\
  -b, --bus-summary          Summarize by bus.\n\
  -t, --type-summary         Summarize by device type.\n\
  --data-path PATH           Print the data matching PATH.\n\
\n\
OPTIONS\n\
  -d, --disable-bus BUS      Disable the bus BUS.\n\
  -e, --enable-bus BUS       Enable the bus BUS.\n\
  --data-version VERSION     Print only data matching VERSION.\n\
  --insert-url URL           Insert URL in front of the list of data sources.\n\
  --append-url URL           Append URL at the end of the list of data sources.\n\
  --[no-]vendor              Do [not] print the vendor name.\n\
  --[no-]vendor-id           Do [not] print the bus-specific vendor ID.\n\
  --[no-]model               Do [not] print the model name.\n\
  --[no-]model-id            Do [not] print the bus-specific model ID.\n\
  --normalize-whitespace     Condense all whitespace into individual spaces\n\
                             and eliminate leading and trailing whitespace.\n\
  --format TEMPLATE          Format the query data output.\n\
  -v, --verbose              Print verbose output.\n\
");
    printf("Report bugs to <%s>.\n", PACKAGE_BUGREPORT);
}

static void
print_or_unknown(char *s)
{
    if (s) {
        printf("%s ", s);
    } else {
        fputs("unknown ", stdout);
    }
}

static void
print_device_list(discover_device_t *device)
{
    int i = 0;
    for (i = 0; device; device = discover_device_get_next(device), i++) {
        if (strcmp(discover_device_get_model_id(device), "default") == 0) {
            if (discover_device_get_next(device) || i != 0) {
                continue;
            }
        }
        if (do_vendor_id) {
            print_or_unknown(discover_device_get_vendor_id(device));
        }
        if (do_model_id) {
            print_or_unknown(discover_device_get_model_id(device));
        }

        if (do_vendor) {
            print_or_unknown(discover_device_get_vendor_name(device));
        }
        if (do_model) {
            print_or_unknown(discover_device_get_model_name(device));
        }

        putchar('\n');
    }
}

static void
check_status(discover_error_t *status)
{
    if (verbose) {
        if (status->message) {
            fprintf(stderr, "Status message: %s\n", status->message);
        }
    }
    if (status->code != 0) {
        switch(status->code) {
            case DISCOVER_EIO:
                break;
            case DISCOVER_EDEVICENOTFOUND:
                    /* This can happen and will on a new install
                     * Don't do anything yet, but someday perhaps
                     */
                break;
            default:
                if (status->code == DISCOVER_ESYS) {
                    err(EX_OSERR, discover_strerror(status));
                } else {
                    errx(status->code, discover_strerror(status));
                }
        }
    }
}

static char *
normalize_text(char *text)
{
    char *end = &(text[strlen(text)-1]);
    char *buffer = (char *)calloc(strlen(text)+1, 1);
    char *bufptr = buffer;

    int last_was_space = 0;

    while (text <= end) {
        if (!isspace(*text)) {
            if (last_was_space) {
                *bufptr = ' ';
                bufptr++;
                last_was_space = 0;
            }
            *bufptr = *text;
            bufptr++;
        } else {
            last_was_space = 1;
        }

        text++;
    }

    /* Not needed (we call calloc), but I'm paranoid... */
    *(bufptr+1) = '\0';

    return buffer;
}

void
format_handler(char **format_fullstring, format_details_t *details)
{
    int state = 0;
    char next_input = 0;
    char *next_output = details->format_substring;

    details->format_substring[0] = '\0';
    details->plain_output = -1;
    details->error_char = -1;

    next_input = **format_fullstring;
    (*format_fullstring)++;

    if (next_input != '%') {
        goto substitute_char;
    }

    *next_output = next_input;
    next_output++;

    next_input = **format_fullstring;
    (*format_fullstring)++;

    while(1) {
        if (next_output - details->format_substring >= FORMAT_LENGTH)
        {
            fprintf(stderr, "Too many characters between %% and s\n");
            exit(EX_USAGE);
        }

        switch(next_input) {
        case '%':
            if (state != 0) {
                goto illegal_char;
            }
            goto substitute_char;

        case 's':
            *next_output = next_input;
            *(next_output + 1) = '\0';
            return;

        case '\0':
            goto illegal_char;

        default:
            state++;

            if (isspace(next_input)) {
                goto illegal_char;
            }

            *next_output = next_input;
            next_output++;
            break;
        }
        next_input = **format_fullstring;
        (*format_fullstring)++;
    }

illegal_char:
    details->error_char = next_input;
    return;

substitute_char:
    details->plain_output = next_input;
    return;
}

static void
ll_print(FILE *stream, char *format, linked_list_t *list)
{
    format_details_t *details = calloc(1, sizeof(format_details_t));
    char *next = format;
    char *list_text = NULL;

    ll_rewind(list);

    while(*next != '\0') {
        format_handler(&next, details);
        if (details->error_char == -1) {
            if (details->plain_output > -1) {
                fputc(details->plain_output, stream);
            } else {
                list_text = ll_next(list);

                if (list_text != NULL) {
                    fprintf(stream, details->format_substring, list_text);
                }
            }
        }
    }
    fputc('\n', stream);
    free(details);
}

/******************************************************************************
 * Actions
 */

static void
bus_summary(char *busname)
{
    discover_bus_t bus;
    discover_bus_map_t *busmap;
    discover_error_t *status;
    discover_device_t *devices;
    int i;


    status = discover_error_new();
    if (busname) {
        bus = discover_conf_name_to_bus(busname, status);
        check_status(status);
        devices = discover_get_devices(bus, status);
        check_status(status);

        print_device_list(devices);
    } else {
        busmap = discover_conf_get_full_bus_map(status);
        check_status(status);
        for (i = 0; busmap[i].name; i++) {
            if (busmap[i].scan_default) {
                devices = discover_get_devices(i, status);
                check_status(status);

                print_device_list(devices);

                memdebug_free(busmap[i]);
            }
        }
    }
    discover_error_free(status);
}

static void
type_summary(char *type)
{
    discover_device_t *devices;
    discover_error_t *status;

    assert(type != NULL);

    status = discover_error_new();
    devices = discover_device_find(type, status);
    check_status(status);
    print_device_list(devices);

    discover_device_free(devices, 0);
    discover_error_free(status);
}

static void
process_data_query(discover_device_t *devices, char *version, discover_error_t *status)
{
    discover_device_t *device;
    char *data;
    char *path;

    for (device = devices; device; device = discover_device_get_next(device)) {
        data_list = ll_new();

        ll_rewind(path_list);

        while((path = ll_next(path_list))) {
            if (format_string == NULL) {
                /*
                 * We'll only test the last path provided on the
                 * command line if no format string was given.
                 */
                path = ll_last(path_list);
            }

            data = discover_device_get_data(device, path, version, status);
            check_status(status);
            if (data) {
                if (normalize_data) {
                    data = normalize_text(data);
                    ll_add(data_list, data);
                    free(data);
                } else {
                    ll_add(data_list, data);
                }
            } else {
                ll_add(data_list, "");
            }

            if (format_string == NULL) {
                break;
            }
        }

        if (format_string == NULL) {
            ll_print(stdout, "%s", data_list);
        } else {
            ll_print(stdout, format_string, data_list);
        }

        ll_free(data_list);
    }
}

static discover_device_t *
find_device_by_id(char *id, discover_error_t *status)
{
    discover_bus_map_t *busmap;
    discover_device_t *devices, *device;
    discover_device_t *result;
    int i;

    /* XXX this is a really inefficient way to do this, but i want to
       avoid an api change for now */
    busmap = discover_conf_get_full_bus_map(status);
    check_status(status);
    result = NULL;
    for (i = 0; busmap[i].name; i++) {
        if (busmap[i].scan_default) {
            devices = discover_get_devices(i, status);
            check_status(status);
            for (device = devices; device; device = discover_device_get_next(device)) {
                char buf[11];    /* vendorid + modelid */
                sprintf(buf, "0x%4.4s%4.4s",
                        discover_device_get_vendor_id(device),
                        discover_device_get_model_id(device));
                if (strcmp(buf, id) == 0) {
                    result = discover_device_new();
                    discover_device_copy(device, result);
                    result->next = NULL;
                    break;
                }
            }
            discover_device_free(devices, 0);
            memdebug_free(busmap[i]);
        }
    }

    return result;
}

static void
query_data(char *version, int argc, char **argv)
{
    discover_bus_map_t *busmap;
    discover_device_t *devices, *device;
    discover_error_t *status;
    int i, j;

    assert(argv != NULL);

    status = discover_error_new();
    if (argc == 0) {
        /* print information for all busses */
        busmap = discover_conf_get_full_bus_map(status);
        check_status(status);
        for (i = 0; busmap[i].name; i++) {
            if (busmap[i].scan_default) {
                devices = discover_get_devices(i, status);
                check_status(status);
                process_data_query(devices, version, status);
                check_status(status);
                discover_device_free(devices, 0);
                memdebug_free(busmap[i]);
            }
        }
    }
    for(i = 0; i < argc; i++) {
        /* if ARGV[I] starts with "0x", it's a device id */
        if (strncmp(argv[i], "0x", 2) == 0) {
            devices = find_device_by_id(argv[i], status);
        } else {
            devices = discover_device_find(argv[i], status);
        }
        check_status(status);
        process_data_query(devices, version, status);
        check_status(status);
        discover_device_free(devices, 0);
    }
    discover_error_free(status);
}

int
main(int argc, char *argv[])
{
    discover_bus_map_t *busmap;
    discover_error_t *status;
    enum action action;
    bool element_found;
    char *version;
    int ch, optindex;
    int i, j;

    action = BUS_SUMMARY;
    version = NULL;

    path_list = ll_new();
    status = discover_error_new();

    while ((ch = getopt_long(argc, argv, shortopts, longopts,
                             &optindex)) != -1) {
        switch (ch) {
        case 'b':
            action = BUS_SUMMARY;
            break;
        case 'd':
            if (strcmp(optarg, "all") == 0) {
                busmap = discover_conf_get_full_bus_map(status);
                check_status(status);
                for (i = 0; busmap[i].name; i++) {
                    busmap[i].scan_default = 0;
                    if (verbose) {
                        fprintf(stderr, "Disabled %s\n", busmap[i].name);
                    }
                }
            } else {
                busmap = discover_conf_get_bus_map_by_name(optarg, status);
                check_status(status);
                busmap->scan_default = 0;
                if (verbose) {
                    fprintf(stderr, "Disabled %s\n", optarg);
                }
            }
            break;
        case 'e':
            if (strcmp(optarg, "all") == 0) {
                busmap = discover_conf_get_full_bus_map(status);
                check_status(status);
                for (i = 0; busmap[i].name; i++) {
                    busmap[i].scan_default = 1;
                    if (verbose) {
                        fprintf(stderr, "Enabled %s\n", busmap[i].name);
                    }
                }
            } else {
                busmap = discover_conf_get_bus_map_by_name(optarg, status);
                check_status(status);
                busmap->scan_default = 1;
                if (verbose) {
                    fprintf(stderr, "Enabled %s\n", optarg);
                }
            }
            break;
        case 'h':
            print_help();
            exit(0);
            break;
        case 't':
            action = TYPE_SUMMARY;
            break;
        case 'V':
            print_version();
            exit(0);
            break;
        case 'v':
            verbose = 1;
            break;
        case 0:
            /* Handled by getopt_long itself (according to the
             * structure we pass it.
             */
            break;
        case 1:
            if (strcmp(longopts[optindex].name, "data-path") == 0) {
                action = QUERY_DATA;
                ll_add(path_list, optarg);
            } else if (strcmp(longopts[optindex].name, "data-version") == 0) {
                version = optarg;
            } else if (strcmp(longopts[optindex].name, "insert-url") == 0) {
                if (verbose) {
                    fprintf(stderr, "Inserting URL %s ... ", optarg);
                }
                discover_conf_insert_url(optarg, status);
                check_status(status);
                if (verbose) {
                    fputs("Done\n", stderr);
                }
            } else if (strcmp(longopts[optindex].name, "append-url") == 0) {
                if (verbose) {
                    fprintf(stderr, "Appending URL %s ... ", optarg);
                }
                discover_conf_append_url(optarg, status);
                check_status(status);
                if (verbose) {
                    fputs("Done\n", stderr);
                }
            } else if (strcmp(longopts[optindex].name, "normalize-whitespace")
                       == 0) {
                if (verbose) {
                    fputs("Will normalize query data\n", stderr);
                }
                normalize_data = 1;
            } else if (strcmp(longopts[optindex].name, "format") == 0) {
                format_string = optarg;
            } else {
                print_usage();
                exit(EX_USAGE);
            }
            break;
        default:
            print_usage();
            exit(EX_USAGE);
            break;
        }
    }
    argc -= optind;
    argv += optind;

    if (version && action != QUERY_DATA) {
        fputs("--data-version has no meaning without --data-path.\n\n",
              stderr);
        print_usage();
        exit(EX_USAGE);
    }

    /* If the user wants verbose output, explicitly load this stuff
     * now so we can report status rather than having it loaded lazily
     * behind the scenes later.
     */
    if (verbose) {

        element_found = false;

        busmap = discover_conf_get_full_bus_map(status);
        check_status(status);
        fputs("Loading XML data... ", stderr);
        for (i = 0; busmap[i].name; i++) {
            for (j = 0; j < argc; j++) {
                if (!strncmp(argv[j], busmap[i].name,
                        strlen(argv[j]))) {
                    busmap[i].scan_default = true;
                }
            }
            if (busmap[i].scan_default) {
                element_found = true;
                fprintf(stderr, "%s ", busmap[i].name);
                discover_xml_get_devices(i, status);
                check_status(status);
            }
        }
        if(element_found) {
            fputs("Done\n", stderr);
        } else {
            fputs("No busmaps loaded\n", stderr);
            element_found = false;
        }

    }

    switch (action) {
    case BUS_SUMMARY:

        element_found = false;

        if (verbose) {
            fputs("Scanning buses... ", stderr);
            for (i = 0; busmap[i].name; i++) {
                for (j = 0; j < argc; j++) {
                    if (!strncmp(argv[j], busmap[i].name,
                            strlen(argv[j]))) {
                        busmap[i].scan_default = true;
                    }
                }
                if (busmap[i].scan_default) {
                    element_found = true;
                    fprintf(stderr, "%s ", busmap[i].name);
                    discover_get_devices(i, status);
                    check_status(status);
                }
            }
            if (element_found) {
                fputs("Done\n", stderr);
            } else {
                fputs("No buses scanned\n", stderr);
            }
        }

        if (argc == 0) {
            bus_summary(NULL);
        } else {
            for (i = 0; i < argc; i++) {
                bus_summary(argv[i]);
            }
        }
        break;

    case QUERY_DATA:
        query_data(version, argc, argv);
        break;

    case TYPE_SUMMARY:
        if (argc == 0) {
            /* Without arguments we summarize all devices on the
             * system; bus_summary already does that so let it do the
             * work.
             */
            bus_summary(NULL);
        } else {
            for (i = 0; i < argc; i++) {
                type_summary(argv[i]);
            }
        }
        break;

    default:
        fprintf(stderr, "Internal error: impossible action %d\n", action);
        exit(EX_SOFTWARE);
        break;
    }

    ll_free(path_list);
    discover_error_free(status);
    return 0;
}

/*
 * Local variables:
 * c-file-style: "progeny"
 * indent-tabs-mode: nil
 * End:
 */
/* vim: set cin fo=tcroq sw=4 et sts=4 tw=75: */
