/**
 * @file version-cmp.c
 * @brief Version comparison functions
 *
 * This file holds the routines needed to process version data in the XML
 * file.  This version data is used to look for information
 * pertaining to a particular version of the OS/software requiring that
 * information.
 */

/* $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 package should include a state machine diagram that
 * is important to understanding this code.
 */

#include "config.h"

#include <assert.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <discover/discover.h>

#include <discover/utils.h>

/** This probably shouldn't be here; it's bad practice (3.0, right). */
#define MAXBUFLEN 256

/** Version placeholders */
typedef enum range_states {
    START,
    OPEN,
    VERSION1,
    VERSEP1,
    PAUSE1,
    SEPARATOR,
    I, N, F,
    VERSION2,
    VERSEP2,
    PAUSE2,
    CLOSE,
    ERROR
}
/** This is used as a placeholder while comparing versions of modules. */
range_state;

/*
 * For now, we only understand '.' as a separator, but that could
 * change.
 */
static int
is_versep_char(char c)
{
    return c == '.';
}

/*
 * Wrapper for isdigit(), in case we want to add more legitimate
 * version characters later.
 */
static int
is_version_char(char c)
{
    return isdigit(c);
}

/*
 * We only handle dot-separated integers. Anything else becomes
 * subject to interpretation.
 *
 * Return values:
 *   1 - v1 is higher
 *   0 - v2 is higher
 *  -1 - v1, v2 are equivalent
 *
 * The string "inf" represents an undefined upper bound; the equivalent on
 * the lower bound should be 0.
 */
static int
version_compare(const char *const v1, const char *const v2)
{
    int int1, int2;
    char *word1, *word2;
    char *version1, *version2;
    char *buffer1;
    char *buffer2;

    if (strcmp(v1, v2) == 0) {
        return -1;
    }

    if (strcmp(v1, "inf") == 0) {
        return 1;
    }

    if (strcmp(v2, "inf") == 0) {
        return 0;
    }

    buffer1 = _discover_xmalloc(MAXBUFLEN);
    buffer2 = _discover_xmalloc(MAXBUFLEN);

    version1 = _discover_xstrdup(v1);
    version2 = _discover_xstrdup(v2);

    word1 = strtok_r(version1, ".", &buffer1);
    word2 = strtok_r(version2, ".", &buffer2);

    do {
        sscanf(word1, "%d", &int1);
        sscanf(word2, "%d", &int2);

        if (int1 != int2) {
            return int1 > int2;
        }

        word1 = strtok_r(NULL, ".", &buffer1);
        word2 = strtok_r(NULL, ".", &buffer2);

    } while(word1 != NULL && word2 != NULL);

    free(version1);
    free(version2);

    if (word2 == NULL && word1 == NULL) {
        return -1;
    }
    if (word2 == NULL) {
        return 1;
    }
    return -0;
}


static int
determine_range(char * range_text, char **upper,
                int *upper_inclusive, char **lower, int *lower_inclusive,
                char **next)
{
    char *current = range_text;
    char c;
    char last_char = '\0';

    char buffer1[MAXBUFLEN];
    char *bufptr1 = buffer1;

    char buffer2[MAXBUFLEN];
    char *bufptr2 = buffer2;

    range_state state = START;
    range_state last_good_state = START;
    int first_inclusive = -1, second_inclusive = -1;

    while(1) {
        c = *current;

        switch(state) {
        case START:
            if (isspace(c)) {
                state = START;
            } else if (c == '(' || c == '[') {
                state = OPEN;
            } else {
                last_good_state = state;
                state = ERROR;
            }
            break;

        case OPEN:
            if (last_char == '[') {
                first_inclusive = 1;
            } else {
                first_inclusive = 0;
            }

            if (isspace(c)) {
                state = OPEN;
            } else if (is_version_char(c)) {
                state = VERSION1;
            } else {
                last_good_state = state;
                state = ERROR;
            }
            break;

        case VERSION1:
            if (bufptr1 - buffer1 > 254) {
                last_good_state = state;
                state = ERROR;
                break;
            }

            *bufptr1 = last_char;
            bufptr1++;

            if (isspace(c) || c == ',') {
                *bufptr1 = '\0';

                if (c == ',') {
                    state = SEPARATOR;
                } else {
                    state = PAUSE1;
                }
            } else if (is_version_char(c)) {
                state = VERSION1;
            } else if (is_versep_char(c)) {
                state = VERSEP1;
            } else {
                last_good_state = VERSION1;
                state = ERROR;
            }
            break;

        case VERSEP1:
            if (bufptr1 - buffer1 > 254) {
                last_good_state = state;
                state = ERROR;
                break;
            }

            *bufptr1 = last_char;
            bufptr1++;

            if (is_version_char(c)) {
                state = VERSION1;
            } else {
                last_good_state = VERSION1;
                state = ERROR;
            }
            break;

        case PAUSE1:
            if (isspace(c)) {
                state = PAUSE1;
            } else if (c == ',') {
                state = SEPARATOR;
            } else {
                last_good_state = PAUSE1;
                state = ERROR;
            }
            break;

        case SEPARATOR:
            if (isspace(c)) {
                state = SEPARATOR;
            } else if (is_version_char(c)) {
                state = VERSION2;
            } else if (c == 'i') {
                state = I;
            } else {
                last_good_state = SEPARATOR;
                state = ERROR;
            }
            break;

        case I:
            if (c == 'n') {
                state = N;
            } else {
                last_good_state = state;
                state = ERROR;
            }
            break;

        case N:
            if (c == 'f') {
                state = F;
            } else {
                last_good_state = state;
                state = ERROR;
            }
            break;

        case F:
            strcpy(bufptr2, "inf");
            if (isspace(c)) {
                state = PAUSE2;
            } else if (c == ')' || c == ']') {
                state = CLOSE;
            } else {
                last_good_state = state;
                state = ERROR;
            }
            break;

        case VERSION2:
            if (bufptr2 - buffer2 > 254) {
                last_good_state = state;
                state = ERROR;
                break;
            }

            *bufptr2 = last_char;
            bufptr2++;

            if (isspace(c) || c == ')' || c == ']') {
                *bufptr2 = '\0';

                if (c == ')' || c == ']') {
                    state = CLOSE;
                } else {
                    state = PAUSE2;
                }
            } else if (is_version_char(c)) {
                state = VERSION2;
            } else if (is_versep_char(c)) {
                state = VERSEP2;
            } else {
                last_good_state = VERSION2;
                state = ERROR;
            }
            break;

        case VERSEP2:
            if (bufptr2 - buffer2 > 254) {
                last_good_state = state;
                state = ERROR;
                break;
            }

            *bufptr2 = last_char;
            bufptr2++;

            if (is_version_char(c)) {
                state = VERSION2;
            } else {
                last_good_state = VERSION2;
                state = ERROR;
            }
            break;

        case PAUSE2:
            if (isspace(c)) {
                state = PAUSE2;
            } else if (c == ')' || c == ']') {
                state = CLOSE;
            } else {
                last_good_state = PAUSE2;
                state = ERROR;
            }
            break;

        case CLOSE:
            *next = current;

            if (last_char == ')') {
                second_inclusive = 0;
            } else if (last_char == ']') {
                second_inclusive = 1;
            }

            if (version_compare(buffer1, buffer2) == 1) {
                *upper = _discover_xmalloc(bufptr1 - buffer1 + 1);
                strcpy(*upper, buffer1);
                *upper_inclusive = first_inclusive;

                *lower = _discover_xmalloc(bufptr2 - buffer2 + 1);
                strcpy(*lower, buffer2);
                *lower_inclusive = second_inclusive;
            } else {
                *lower = _discover_xmalloc(bufptr1 - buffer1 + 1);
                strcpy(*lower, buffer1);
                *lower_inclusive = first_inclusive;

                *upper = _discover_xmalloc(bufptr2 - buffer2 + 1);
                strcpy(*upper, buffer2);
                *upper_inclusive = second_inclusive;
            }

            /* EXIT POINT */
            return 1;

        default:
            state = ERROR;
            last_good_state = -1;
            break;
        }

        if (state == ERROR) {
            /* Do something. */
            return 0;
        }

        /*
         * We don't care about recording spaces; let's simplify by not
         * doing so, in case we'd be missing something valuable.
         */
        if (!isspace(*current)) {
            last_char = *current;
        }

        current++;
    }
}

/**
 * @defgroup version_cmp Version comparison
 * @{
 */

/**
 * Compare a version number against a version range, returning 1 if
 * the number matches the range or 0 if not.
 *
 * @param range Range to compare against version
 * @param version Version to compare against range
 * @param status Address in which to place status report
 */
int
discover_xml_version_cmp(char *range, char *version, discover_error_t *status)
{
    char *upper, *lower;
    int upper_inclusive, lower_inclusive;
    char *next_range;
    int range_count = 0;

    assert(range != NULL);
    assert(version != NULL);
    assert(status != NULL);

    next_range = range;

    while(*next_range != '\0') {
        range_count++;
        if (determine_range(next_range, &upper, &upper_inclusive,
                            &lower, &lower_inclusive, &next_range)) {
            /*
             * First test: Does this version match the upper end of the
             * range exactly? If so, is the upper end of the range
             * inclusive?
             *
             * Second test: Does this version match the lower end of the
             * range? If so, is the lower end inclusive?
             *
             * Finally, is the version higher than the lower end, and lower
             * then the upper end?
             */
            if (version_compare(version, upper) == -1
                && upper_inclusive) {
                return range_count;
            } else if (version_compare(version, lower) == -1
                       && lower_inclusive) {
                return range_count;
            } else if (version_compare(version, lower) > 0 &&
                       version_compare(upper, version) > 0) {
                return range_count;
            }

        } else {
            status->code = DISCOVER_EBADVERSION;
            return -1;
        }
    }

    return 0;
}

/** @} */
