/**
 * @file device-xml.c
 * @brief Device XML data file parsing
 *
 * This file contains the routines needed to properly process the device
 * XML data.  This file is responsible for handling URLs and
 * storing the XML data during the parsing process.
 */

/* $Progeny$
 *
 * AUTHOR: John R. Daily <jdaily@progeny.com>
 *
 * Copyright 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.
 */

/*
 * This is slightly different than busclass-xml.c and vendor-xml.c.
 * With those, the data that interests us is actually in the
 * XML files, so the structures built by those modules are the final
 * structures.

 * With the devices, the list we build here is only an intermediate
 * place to store the discover_device structures.  When we probe the
 * bus, we will move the discover_device structures for which
 * we have found corresponding hardware from this list to the final
 * one. (That action happens in the modules for the individual buses,
 * such as pci.c.)

 * Whereas busclass-xml.c and vendor-xml.c create pci_busclasses and
 * pci_vendors lists, respectively, this module creates
 * pci_devices_xml.  Objects are moved from this list to the
 * pci_devices list in pci.c, and the rest are freed.
 */

#include "config.h"

#include <sys/types.h>

#include <assert.h>
#include <ctype.h>
#include <limits.h>
#include <stdbool.h>
#include <string.h>
#include <errno.h>

#include <expat.h>

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

#include <discover/load-url.h>
#include <discover/device.h>
#include <discover/utils.h>
#include <discover/stack.h>

/** Length (including terminating null) of model and vendor ID strings */
#define IDLEN 5

static discover_device_t *devices_xml[BUS_COUNT];

/** Representative of the state within the XML data */
enum state { START, FINISH, DEVICE, DATA };
struct context {
    enum state state;

    discover_xml_busclass_t *busclasses;
    discover_xml_vendor_t *vendors;

    /* Device stuff */
    discover_device_t **dhead;
    discover_device_t *dtail;

    /* data stuff */
    discover_xml_stack *stack;
/*
    discover_data_t *data_current;
    discover_data_t *data_parent;
*/
    int nesting;
    int last_nesting;

    int unknown_level; /* How deep are we into unknown XML tags? */
};

static char *known_device_elements[] = {
    "data",
    "device",
    "device_list",
    NULL
};

static void
get_data_failure_handler(discover_error_t **status, char *url)
{
    char *errmsg;
    static int maxurlsize = 1024; /* If your URL is longer than this,
                                     you've got problems */

    if((*status)->code == DISCOVER_EIO) {
        errmsg = _discover_xmalloc(maxurlsize + 1);
        snprintf(errmsg, maxurlsize, "Resource not found: %s", url);
        (*status)->create_message(status, errmsg);
        free(errmsg);
    } else {
        if (errno) {
            errmsg = _discover_xmalloc(maxurlsize + 1);
            snprintf(errmsg, maxurlsize, "Problem loading resource: %s: %s",
                     strerror(errno), url);
            (*status)->create_message(status, errmsg);
            free(errmsg);
        } else {
            (*status)->create_message(status,
                    "Unknown failure from system-dependent libraries");
        }
    }
}

static bool
unknown_device_element(const XML_Char * const tag)
{
    int i;
    for (i = 0; known_device_elements[i] != NULL; i++) {
        if (strcmp(tag, known_device_elements[i]) == 0)
            return false;
    }
    return true;
}

static void
create_device(struct context *context, const XML_Char *attrs[])
{
    int i;
    char *busclass, *model_id, *model_name, *vendor_id, *vendor_name;
    discover_device_t *new_device;

    assert(context != NULL);
    assert(attrs != NULL);

    busclass = model_id = model_name = vendor_id = NULL;
    for (i = 0; attrs[i]; i += 2) {
        if (strcmp(attrs[i], "busclass") == 0) {
            busclass = (char *)attrs[i + 1];

        } else if (strcmp(attrs[i], "model") == 0) {
            model_id = (char *)attrs[i + 1];

        } else if (strcmp(attrs[i], "model_name") == 0) {
            model_name = (char *)attrs[i + 1];

        } else if (strcmp(attrs[i], "vendor") == 0) {
            vendor_id = (char *)attrs[i + 1];
        }
    }

    assert(model_id != NULL);
    assert(model_name != NULL);
    assert(vendor_id != NULL);

    vendor_name = discover_xml_vendor_id2name(context->vendors, vendor_id);
    assert(vendor_name != NULL);

    context->stack = discover_xml_stack_new();

    new_device = discover_device_new();
    new_device->busclasses = context->busclasses;
    new_device->vendors = context->vendors;
    if (busclass) {
        new_device->busclass = _discover_xstrdup(busclass);
    } else {
        new_device->busclass = NULL;
    }
    new_device->model_id = _discover_xstrdup(model_id);
    new_device->model_name = _discover_xstrdup(model_name);
    new_device->vendor_id = _discover_xstrdup(vendor_id);
    new_device->vendor_name = _discover_xstrdup(vendor_name);
    new_device->data = NULL;
    new_device->next = NULL;

    if (*(context->dhead)) {
        context->dtail->next = new_device;
        context->dtail = new_device;
    } else {
        *(context->dhead) = new_device;
        context->dtail = new_device;
    }
}

static void
create_data(struct context *context, const XML_Char *attrs[])
{
    discover_data_t *new_data, *current_data;
    discover_xml_stack *stack;
    int i;
    char *discover_class, *version;

    assert(context != NULL);
    assert(attrs != NULL);

    new_data = discover_data_new();
    new_data->text = NULL;
    new_data->next = new_data->prev = new_data->child = NULL;


    discover_class = version = NULL;
    for (i = 0; attrs[i]; i += 2) {
        if (strcmp(attrs[i], "class") == 0) {
            discover_class = (char *)attrs[i + 1];
        } else if (strcmp(attrs[i], "version") == 0) {
            version = (char *)attrs[i + 1];
        }
    }

    assert(discover_class != NULL);

    new_data->discover_class = _discover_xstrdup(discover_class);
    if (version) {
        new_data->version = _discover_xstrdup(version);
    }

    stack = context->stack;

    assert(stack != NULL);

    current_data = discover_xml_stack_get(stack);

    if(current_data) { /* The first time through this, we have no data. */
        /* First element of the list */
        if(stack->depth > context->nesting) {
            discover_xml_stack_pop(&stack);
            //current_data = discover_xml_stack_pop(&stack);
            new_data->prev = current_data;
            new_data->prev->next = new_data;
            if(context->nesting) {
                new_data->parent =
                    discover_xml_stack_getbynum(stack, context->nesting);
            }
        } else {
            /* Brand new child */
            new_data->parent = current_data;
            new_data->parent->child = new_data;
        }
    }

    discover_xml_stack_push(&stack, new_data);
    context->stack = stack;
}

static void
start_element(void *ctx, const XML_Char *name, const XML_Char *attrs[])
{
    struct context *context = ctx;

    assert(context != NULL);
    assert(name != NULL);
    assert(attrs != NULL);


    if (unknown_device_element(name)) {
        context->unknown_level++;
        return;
    }

    if (context->unknown_level > 0) {
        return;
    }

    switch (context->state) {
    case FINISH:
        return;

    case START:
        if (strcmp(name, "device") == 0) {
            context->state = DEVICE;
            create_device(context, attrs);
        }
        break;

    case DEVICE:
        if (strcmp(name, "data") == 0) {
            context->nesting = context->last_nesting = 0;
            context->state = DATA;
        }
        /* Falls through */

    case DATA:
        if (strcmp(name, "data") == 0) {
            create_data(context, attrs);
            context->last_nesting = context->nesting;
            context->nesting++;
        }
        break;
    }
}

static void
end_element(void *ctx, const XML_Char *name)
{
    struct context *context = ctx;
    discover_device_t *device;
    discover_data_t *current_data;
    discover_xml_stack *stack;


    assert(context != NULL);
    assert(name != NULL);


    if (unknown_device_element(name)) {
        context->unknown_level--;
        return;
    }

    if (context->unknown_level > 0) {
        return;
    }

    switch (context->state) {
    case FINISH:
    case START:
        break;

    case DEVICE:
        context->state = START;
        device = context->dtail;
        stack = context->stack;
        current_data = discover_xml_stack_get(stack);
        device->data = discover_data_get_first(current_data);

        while (discover_xml_stack_pop(&(context->stack))) ;
        discover_xml_stack_destroy(context->stack);
        context->stack = NULL;

        break;

    case DATA:
        context->nesting--;
        stack = context->stack;
        if((context->nesting + 2) <= stack->depth) {
            while((context->nesting + 1 < stack->depth) &&
                    stack->depth > 1) {
                discover_xml_stack_pop(&stack);
            }
            context->stack = stack;
        }

        if (context->nesting == 0) {
            context->state = DEVICE;
        }
    }
}

static void
cdata(void *ctx, const XML_Char *data, int len)
{
    struct context *context = ctx;
    size_t old_len;
    discover_data_t *current_data;

    assert(context != NULL);
    assert(data != NULL);

    if (context->state == DATA
        && context->nesting > context->last_nesting) {
        assert(context->stack != NULL);
        current_data = context->stack->data;
        assert(current_data != NULL);

        if (!current_data->text) {
            old_len = 0;
            current_data->text = _discover_xmalloc(1);
            current_data->text[0] = '\0';
        } else {
            old_len = strlen(current_data->text);
        }
        current_data->text
            = _discover_xrealloc(current_data->text,
                                 old_len + len + 1);
        strncat(current_data->text,
                (const char *)data,
                (unsigned int)len);
    }
}

/**
 * @defgroup device_xml Device list XML parsing
 * @{
 */

/**
 * Merge new busclasses into a list.
 *
 * @param dlist Address of the list to merge busclasses into
 * @param url URL of the document defining the busclasses
 * @param busclasses List of busclasses for this bus
 * @param vendors List of vendors for this bus
 * @param status Address in which to place status report
 */
/* Sshh!  Don't tell, but this doesn't actually do any merging at all.
 * Instead, it simply inserts newer entries at the front of the list,
 * meaning that device info found later supersedes info found earlier.
 * This gives the illusion of merging, but potentially wastes memory
 * with duplicates.
 */
void
discover_xml_merge_device_url(discover_device_t **dlist, char *url,
                              discover_xml_busclass_t *busclasses,
                              discover_xml_vendor_t *vendors,
                              discover_error_t *status)
{
    XML_Parser parser;
    struct context context;

    assert(url != NULL);
    assert(busclasses != NULL);
    assert(vendors != NULL);
    assert(status != NULL);

    context.state = START;
    context.dhead = dlist;
    if (*(context.dhead)) {
        discover_device_t *next = *(context.dhead);
        while(next->next != NULL) {
            next = next->next;
        }
        context.dtail = next;
    } else {
        context.dtail = NULL;
    }

    context.busclasses = busclasses;
    context.vendors = vendors;
    context.unknown_level = 0;
    context.stack = NULL;

    parser = XML_ParserCreate(NULL);
    XML_SetElementHandler(parser, start_element, end_element);
    XML_SetCharacterDataHandler(parser, cdata);
    XML_SetUserData(parser, &context);

    if (!_discover_load_url(url, parser)) {
        XML_ParserFree(parser);
        status->code = DISCOVER_EIO;
        return;
    }

    if (!XML_Parse(parser, "", 0, 1)) {
        XML_ParserFree(parser);
        status->code = DISCOVER_EXML;
        return;
    }

    XML_ParserFree(parser);

    return;
}

/**
 * Get the list of devices for the required bus.
 *
 * @param bus Bus for which the devices are requested
 * @param status Address in which to place status report
 */
discover_device_t *
discover_xml_get_devices(discover_bus_t bus, discover_error_t *status)
{
    discover_xml_url_t *urls, *i;
    char *url;
    discover_xml_busclass_t *busclasses;
    discover_xml_vendor_t *vendors;

    assert(status != NULL);

    status->code = 0;

    if (!devices_xml[bus]) {
        urls = discover_xml_get_data_urls(bus, DEVICE, status);
        if (status->code != 0) {
            return NULL;
        }

        busclasses = discover_xml_get_busclasses(bus, status);
        if (status->code != 0) {
            return NULL;
        }

        vendors = discover_xml_get_vendors(bus, status);
        if (status->code != 0) {
            return NULL;
        }

        for (i = urls;
             i;
             i = discover_xml_url_get_next(i)) {
            url = discover_xml_url_get_url(i);
            discover_xml_merge_device_url(&(devices_xml[bus]), url,
                                          busclasses, vendors, status);
            if (status->code != 0) {
                get_data_failure_handler(&status, url);
            }
        }
    }


    return devices_xml[bus];
}

/**
 * Free the list of XML devices.
 */
void
discover_xml_free_devices(void)
{
    int i;
    for (i = 0; i < BUS_COUNT; i++) {
        discover_device_free(devices_xml[i], 1);
        devices_xml[i] = NULL;
    }
}

/**
 * Find the first device in xml_devices matching target_vendor and/or
 * target_model. (One or both can be specified, but not zero.)
 *
 * @param xml_devices List of devices to search
 * @param target_vendor Vendor for which to search
 * @param target_model Model for which to search
 * @param status Address in which to place status report
 */
discover_device_t *
discover_xml_find_device(discover_device_t *xml_devices,
                         char *target_vendor, char *target_model,
                         discover_error_t *status)
{
    discover_device_t *device;

    assert(target_vendor || target_model);

    for (device = xml_devices;
         device;
         device = device->next) {
        if (target_vendor && target_model) {
            if ((strcmp(device->model_id, target_model) == 0 ||
                 strcmp(device->model_id, "default") == 0)
                && strcmp(device->vendor_id, target_vendor) == 0) {
                break;
            }
        } else if (target_vendor) {
            if (strcmp(device->vendor_id, target_vendor) == 0) {
                break;
            }
        } else {
            /* We only have target_model. */
            if ((strcmp(device->model_id, target_model) == 0)) {
                break;
            }
        }
    }

    return device;
}

/**
 * Find the next device in xml_devices matching target_vendor and/or
 * target_model. (One or both can be specified, but not zero.)
 *
 * The first device in the list is assumed to have been processed,
 * so we simply call discover_xml_find_device() on the next pointer.
 *
 * @param xml_devices List of devices to search
 * @param target_vendor Vendor for which to search
 * @param target_model Model for which to search
 * @param status Address in which to place status report
 */
discover_device_t *
discover_xml_find_next_device(discover_device_t *xml_devices,
                         char *target_vendor, char *target_model,
                         discover_error_t *status)
{
    return discover_xml_find_device(xml_devices->next,
                                    target_vendor, target_model,
                                    status);
}


/**
 * Find and duplicate all devices in xml_devices matching target_vendor
 * and/or target_model. (One or both can be specified, but not zero.)
 *
 * @param xml_devices List of devices to search
 * @param target_vendor Vendor for which to search
 * @param target_model Model for which to search
 * @param status Address in which to place status report
 */
discover_device_t *
discover_xml_get_matching_devices(discover_device_t *xml_devices,
                                  char *target_vendor, char *target_model,
                                  discover_error_t *status)
{
    discover_device_t *device, *last, *copy;
    discover_device_t *head_device = NULL;

    device = discover_xml_find_device(xml_devices, target_vendor,
                                      target_model, status);
    last = NULL;

    while(device) {
        copy = discover_device_new();
        discover_device_copy(device, copy);
        copy->next = NULL;
        copy->extra = NULL;

        if (last) {
            last->next = copy;
        } else {
            head_device = copy;
        }

        last = copy;

        device = discover_xml_find_next_device(device, target_vendor,
                                               target_model, status);
    }

    return head_device;
}

/** @} */

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