/**
 * @file conf.c
 * @brief Configuration handling (including XML parsing)
 *
 * Configuration-related data is handled here.  This file
 * is responsible for loading and parsing the XML data read from
 * the configuration file.  This file also holds the routines that are
 * responsible for altering the URL list as requested by the caller.
 *
 */

/* $Progeny$
 *
 * Copyright 2002-2005 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"

#if HAVE_DIRENT_H
#include <dirent.h>
#define NAMLEN(d) strlen((d)->d_name)
#else
#define dirent direct
#define NAMLEN(d) (d)->d_namlen
#if HAVE_SYS_NDIR_H
#include <sys/ndir.h>
#endif
#if HAVE_SYS_DIR_H
#include <sys/dir.h>
#endif
#if HAVE_NDIR_H
#include <ndir.h>
#endif
#endif

#include <assert.h>
#include <string.h>

#include <expat.h>

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

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

/*Busmaps we understand
 */
static discover_bus_map_t bus_map[] = {
    { "ata", 0, 0, NULL },
    { "pci", 0, 0, NULL },
    { "pcmcia", 0, 0, NULL },
    { "scsi", 0, 0, NULL },
    { "usb", 0, 0, NULL },
    { NULL }
};

static char * filetype_map[] = {
    "vendor", "busclass", "device"
};

static int conf_loaded = 0;

static discover_xml_url_t *pre_urls = NULL;
static discover_xml_url_t *post_urls = NULL;
static discover_xml_url_t *urls = NULL;

/** Describe the current state within the XML structure. */
enum state { START, BUSSCAN, DATA_SOURCES };

/** Define the values for a scannable bus. */
enum scan_flag { SCAN_NEVER, SCAN_DEFAULT };

struct context {
    enum state state;
    enum scan_flag scan;
    discover_error_t *status;

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

static char *known_conf_elements[] = {
    "bus",
    "data-source",
    "busscan",
    "data-sources",
    "conffile",
    NULL
};


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

static enum scan_flag
get_scan_flag(const XML_Char *attrs[])
{
    int i;
    char *scan;

    assert(attrs != NULL);

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

    assert(scan != NULL);

    if (strcmp(scan, "default") == 0) {
        return SCAN_DEFAULT;
    } else {
        return SCAN_NEVER;
    }
}

/** Function that loads the bus map
 *
 * The "real" get_bus_map functions don't call conf_load(), which is
 * important to avoid infinite loops.
 */
discover_bus_map_t *
_real_discover_conf_get_bus_map(discover_bus_t bus, discover_error_t *status)
{
    assert(status != NULL);
    assert(bus >= 0);

    if (bus >= BUS_COUNT) {
        status->code = DISCOVER_EBUSNOTFOUND;
    }

    return (discover_bus_map_t *)(bus_map + bus);
}

/** Internal function that really loads the bus map */
discover_bus_map_t *
_real_discover_conf_get_bus_map_by_name(char *name, discover_error_t *status)
{
    discover_bus_t bus;

    assert(status != NULL);

    bus = discover_conf_name_to_bus(name, status);

    if (status->code != 0) {
        return NULL;
    }

    return _real_discover_conf_get_bus_map(bus, status);
}

static void
_real_discover_conf_insert_url(char *url, discover_error_t *status)
{
    discover_xml_url_t *new;

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

    status->code = 0;

    new = discover_xml_url_new();
    new->url = _discover_xstrdup(url);

    new->next = pre_urls;
    if (pre_urls) {
        if (pre_urls->last) {
            new->last = pre_urls->last;
        } else {
            new->last = pre_urls;
        }
    } else {
        new->last = NULL;
    }

    pre_urls = new;
}

static void
_real_discover_conf_append_url(char *url, discover_error_t *status)
{
    discover_xml_url_t *new;

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

    status->code = 0;

    new = discover_xml_url_new();
    new->url = _discover_xstrdup(url);

    if (post_urls) {
        if (post_urls->last) {
            post_urls->last->next = new;
        } else {
            post_urls->next = new;
        }
        post_urls->last = new;
    } else {
        post_urls = new;
        post_urls->next = NULL;
        post_urls->last = NULL;
    }
}

static void
start_element(void *ctx, const XML_Char *name, const XML_Char *attrs[])
{
    struct context *context = ctx;
    discover_bus_map_t *busmap;
    discover_error_t *status;
    char *busname;
    char *found_url;
    int prepend_url;
    int i;

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

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

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

    status = context->status;

    switch (context->state) {
    case START:
        if (strcmp(name, "busscan") == 0) {
            context->state = BUSSCAN;
            context->scan = get_scan_flag(attrs);
        } else if (strcmp(name, "data-sources") == 0) {
            context->state = DATA_SOURCES;
        }
        break;

    case BUSSCAN:
        if (strcmp(name, "bus") == 0) {
            assert(attrs != NULL);

            for (i = 0; attrs[i]; i += 2) {
                if (strcmp(attrs[i], "name") == 0) {
                    busname = (char *)attrs[i + 1];
                    busmap = _real_discover_conf_get_bus_map_by_name(busname, status);
                    if (status->code != 0) {
                        return;
                    }
                    if (context->scan == SCAN_DEFAULT) {
                        busmap->scan_default = 1;
                    } else {
                        busmap->scan_never = 1;
                    }
                }
            }
        }
        break;

    case DATA_SOURCES:
        if (strcmp(name, "data-source") == 0) {
            found_url = NULL;
            prepend_url = 0;
            for (i = 0; attrs[i]; i += 2) {
                if (strcmp(attrs[i], "url") == 0) {
                    found_url = (char *)attrs[i + 1];
                }
                else if ((strcmp(attrs[i], "place") == 0) &&
                         (strcmp(attrs[i + 1], "before") == 0)) {
                    prepend_url = 1;
                }
            }
            if (found_url != NULL) {
                if (prepend_url != 0) {
                    _real_discover_conf_insert_url(found_url, status);
                } else {
                    _real_discover_conf_append_url(found_url, status);
                }
                if (status->code != 0) {
                    return;
                }
            }
        }
        break;
    }
}

static void
end_element(void *ctx, const XML_Char *name)
{
    struct context *context = ctx;

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

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

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

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

    case BUSSCAN:
        if (strcmp(name, "busscan") == 0) {
            context->state = START;
        }
    break;

    case DATA_SOURCES:
        if (strcmp(name, "data-sources") == 0) {
            context->state = START;
        }
    break;
    }
}

/**
 * @defgroup conf Configuration handling
 * @{
 */

/**
 * Ensure that the configuration file is loaded.  Repeated calls to this
 * function will not change anything, unless you call
 * discover_conf_free.
 *
 * Technically, this function will lie to you if there is no conf
 * file.  Even though no configuration was loaded, repeated calls still
 * will not change anything.  However, if we don't do this and a
 * config file is created between the time some long-lived caller
 * first calls discover_conf_load and the time it calls it again, it
 * will be loaded.  This doesn't strike me as idempotent behavior.
 * So, repeated calls even in this case will not change anything
 * unless you call discover_conf_free.
 *
 * @param status Address in which to place status report
 */
void
discover_conf_load(discover_error_t *status)
{
    XML_Parser parser;
    struct context context;
    int load_url_status;
    int conf_load_error = 0;
    int conf_parse_error = 0;
    DIR *confdir;
    struct dirent *confent;
    char buf[512];

    assert(status != NULL);

    status->code = 0;

    if (conf_loaded) {
        return;
    }

    confdir = opendir(SYSCONFDIR "/discover.conf.d");
    if (confdir != NULL) {
        while ((confent = readdir(confdir)) != NULL) {
            if (strchr(confent->d_name, '.') != NULL) {
                continue;
            }

            context.state = START;
            context.status = status;
            context.unknown_level = 0;

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

            strcpy(buf, "file:///" SYSCONFDIR "/discover.conf.d/");
            strncat(buf, confent->d_name, sizeof(buf));
            buf[sizeof(buf) - 1] = '\0';

            load_url_status = _discover_load_url(buf, parser);

            /* start_element may change status. */
            if (status->code != 0) {
                char *message = _discover_xmalloc(256);
                snprintf(message, 256, "Error parsing configuration file %s", 
                         buf);
                status->create_message(&status, message);
                XML_ParserFree(parser);
                conf_parse_error = 1;
                break;
            }

            /* Ignore URL load errors; the conf file is not mandatory. */
            if (!load_url_status) {
                conf_load_error = 1;
                XML_ParserFree(parser);
                continue;
            }

            if (!XML_Parse(parser, "", 0, 1)) {
                char *message = _discover_xmalloc(256);
                snprintf(message, 256, "Error parsing configuration file %s", 
                         buf);
                status->create_message(&status, message);
                status->code = DISCOVER_EXML;
                XML_ParserFree(parser);
                conf_parse_error = 1;
                break;
            }


            XML_ParserFree(parser);
        }

        closedir(confdir);

        if (conf_parse_error) {
            return;
        }
    }

    if (conf_load_error) {
        char *message = _discover_xmalloc(256);
        snprintf(message, 256, 
                 "Failed loading one or more configuration files");
        status->create_message(&status, message);
    }

    conf_loaded = 1;

    return;
}

/**
 * Get the bus enumeration for a bus name.
 *
 * @param name Name of the bus
 * @param status Address in which to place status report
 */
int
discover_conf_name_to_bus(char *name, discover_error_t *status)
{
    int i;
    for (i = 0; bus_map[i].name; i++) {
        if (strcmp(bus_map[i].name, name) == 0) {
            return i;
        }
    }

    status->code = DISCOVER_EBUSNOTFOUND;
    return -1;
}

/**
 * Get the full bus map, listing all buses.
 *
 * @param status Address in which to place status report
 */
discover_bus_map_t *
discover_conf_get_full_bus_map(discover_error_t *status)
{
    discover_conf_load(status);
    return bus_map;
}

/**
 * Get the bus map by name for a single bus.
 *
 * @param name Name of the bus whose map to get
 * @param status Address in which to place status report
 */
discover_bus_map_t *
discover_conf_get_bus_map_by_name(char *name, discover_error_t *status)
{
    assert(status != NULL);

    discover_conf_load(status);
    if (status->code != 0) {
        return NULL;
    }

    /*
     * The "real" one won't call conf_load(), which will be useful
     * elsewhere.
     */
    return _real_discover_conf_get_bus_map_by_name(name, status);
}

/**
 * Get the bus map for a single bus.
 *
 * @param bus Name of the bus whose map to get
 * @param status Address in which to place status report
 */
discover_bus_map_t *
discover_conf_get_bus_map(discover_bus_t bus, discover_error_t *status)
{
    assert(status != NULL);
    assert(bus >= 0);

    if (bus >= BUS_COUNT) {
        status->code = DISCOVER_EBUSNOTFOUND;
    }

    discover_conf_load(status);
    if (status->code != 0) {
        return NULL;
    }

    return (discover_bus_map_t *)(bus_map + bus);
}

/**
 * Insert the URL specified in url at the head of the list of URLs
 * where Discover data will be retrieved.
 *
 * @param url URL to insert
 * @param status Address in which to place status report
 **/
void
discover_conf_insert_url(char *url, discover_error_t *status)
{
    assert(status != NULL);

    discover_conf_load(status);
    if (status->code != 0) {
        return;
    }

    _real_discover_conf_insert_url(url, status);

    return;
}

/**
 * Append the URL specified in url at the tail of the list of URLs
 * where Discover data will be retrieved.
 *
 * @param url URL to append
 * @param status Address in which to place status report
 **/
void
discover_conf_append_url(char *url, discover_error_t *status)
{
    assert(status != NULL);

    discover_conf_load(status);
    if (status->code != 0) {
        return;
    }

    _real_discover_conf_append_url(url, status);

    return;
}

/**
 * Get the list of URLs from which XML data will be retrieved.
 *
 * @param status Address in which to place status report
 **/
discover_xml_url_t *
discover_conf_get_urls(discover_error_t *status)
{
    discover_xml_url_t *new;

    assert(status != NULL);

    status->code = 0;

    if (!urls) {
        discover_conf_load(status);

        new = discover_xml_url_new();
        new->url = _discover_xstrdup(DISCOVER_DEFAULT_URL);

        if (pre_urls) {
            urls = pre_urls;
            if (urls->last) {
                urls->last->next = new;
            } else {
                urls->next = new;
            }
            urls->last = new;
        } else {
            urls = new;
        }

        if (post_urls) {
            if (urls->last) {
                urls->last->next = post_urls;
            } else {
                urls->next = post_urls;
            }

            if (post_urls->last) {
                urls->last = post_urls->last;
            } else {
                urls->last = post_urls;
            }
        }

        post_urls = pre_urls = NULL;
    }

    return urls;
}

/**
 * Free configuration data.  This is mainly useful so that the next
 * call to discover_conf_load will reload the configuration data.
 */
void
discover_conf_free(void)
{
    conf_loaded = 0;

    if (urls) {
        discover_xml_url_free(urls);
        urls = NULL;
    }
}

/**
 * Translate an enum bus value into a string.
 *
 * @param bus Bus type in enumerated form
 * */
char *
discover_conf_get_bus_name(discover_bus_t bus)
{
    assert(bus >= 0);
    if (bus >= BUS_COUNT) {
        return NULL;
    }

    return bus_map[bus].name;
}

/**
 * Translate an enum filetype value into a string.
 *
 * @param filetype Filetype in enumerated form
 * */
char *
discover_conf_get_filetype_name(discover_filetype_t filetype)
{
    if (filetype >= 3) {
        return NULL;
    }

    return filetype_map[filetype];
}

/** @} */

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