/*
 * vim:ts=4:sw=4:expandtab
 *
 * Copyright © 2016 Ingo Bürk
 *
 * 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
 * AUTHORS 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.
 *
 * Except as contained in this notice, the names of the authors or their
 * institutions shall not be used in advertising or otherwise to promote the
 * sale, use or other dealings in this Software without prior written
 * authorization from the authors.
 *
 */
#include "externals.h"

#include "entry.h"
#include "util.h"

#define BUFFER_SIZE 1024

/**
 * Appends a single character to the current buffer.
 * If the buffer is not yet initialized or has been invalidated, it will be set up.
 *
 */
static void xcb_xrm_append_char(xcb_xrm_entry_t *entry, xcb_xrm_entry_parser_state_t *state,
        const char str) {
    ptrdiff_t offset;

    if (state->buffer_pos == NULL) {
        FREE(state->buffer);
        state->buffer = calloc(1, BUFFER_SIZE);
        state->buffer_pos = state->buffer;
        if (state->buffer == NULL) {
            /* Let's ignore this character and try again next time. */
            return;
        }
    }

    /* Increase the buffer if necessary. */
    offset = state->buffer_pos - state->buffer;
    if (offset % BUFFER_SIZE == BUFFER_SIZE - 1) {
        state->buffer = realloc(state->buffer, offset + BUFFER_SIZE + 1);
        state->buffer_pos = state->buffer + offset;
    }

    *(state->buffer_pos++) = str;
}

/**
 * Insert a new component of the given type.
 * This function does not check whether there is an open buffer.
 *
 */
static void xcb_xrm_insert_component(xcb_xrm_entry_t *entry, xcb_xrm_component_type_t type,
        xcb_xrm_binding_type_t binding_type, const char *str) {
    xcb_xrm_component_t *new = calloc(1, sizeof(struct xcb_xrm_component_t));
    if (new == NULL)
        return;

    if (str != NULL) {
        new->name = strdup(str);
        if (new->name == NULL) {
            FREE(new);
            return;
        }
    }

    new->type = type;
    new->binding_type = binding_type;
    TAILQ_INSERT_TAIL(&(entry->components), new, components);
}

/**
 * Finalize the current buffer by writing it into a component if necessary.
 * This function also resets the buffer to a clean slate.
 *
 */
static void xcb_xrm_finalize_component(xcb_xrm_entry_t *entry, xcb_xrm_entry_parser_state_t *state) {
    if (state->buffer_pos != NULL && state->buffer_pos != state->buffer) {
        *(state->buffer_pos) = '\0';
        xcb_xrm_insert_component(entry, CT_NORMAL, state->current_binding_type, state->buffer);
    }

    FREE(state->buffer);
    /* No need to handle NULL for this calloc call. */
    state->buffer = calloc(1, BUFFER_SIZE);
    state->buffer_pos = state->buffer;
    state->current_binding_type = BT_TIGHT;
}

/*
 * Parses a specific resource string.
 *
 * @param str The resource string.
 * @param entry A return struct that will contain the parsed resource. The
 * memory will be allocated dynamically, so it must be freed.
 * @param resource_only If true, only components of type CT_NORMAL are allowed.
 *
 * @return 0 on success, a negative error code otherwise.
 *
 */
int xcb_xrm_entry_parse(const char *_str, xcb_xrm_entry_t **_entry, bool resource_only) {
    char *str;
    xcb_xrm_entry_t *entry = NULL;
    xcb_xrm_component_t *last;
    char *value;
    char *value_walk;
    xcb_xrm_binding_type_t binding_type;

    xcb_xrm_entry_parser_state_t state = {
        .chunk = CS_INITIAL,
        .current_binding_type = BT_TIGHT,
    };

    /* Copy the input string since it's const. */
    str = strdup(_str);
    if (str == NULL)
        return -FAILURE;

    /* This is heavily overestimated, but we'll just keep it simple here.
     * While this does not account for replacement of magic values, those only
     * make the resulting string shorter than the input, so we're okay. */
    value = calloc(1, strlen(str));
    if (value == NULL) {
        FREE(str);
        return -FAILURE;
    }
    value_walk = value;

    /* Allocate memory for the return parameter. */
    *_entry = calloc(1, sizeof(struct xcb_xrm_entry_t));
    if (_entry == NULL) {
        FREE(str);
        FREE(value);
        return -FAILURE;
    }

    entry = *_entry;
    TAILQ_INIT(&(entry->components));

    for (char *walk = str; *walk != '\0'; walk++) {
        switch (*walk) {
            case '.':
            case '*':
                state.chunk = MAX(state.chunk, CS_COMPONENTS);
                if (state.chunk >= CS_PRE_VALUE_WHITESPACE) {
                    goto process_normally;
                }

                if (*walk == '*' && resource_only) {
                    goto done_error;
                }

                /* Subsequent bindings must be collapsed into a loose binding if at
                 * least one was a loose binding and a tight binding otherwise. */
                binding_type = (*walk == '*') ? BT_LOOSE : BT_TIGHT;
                while (*(walk + 1) == '.' || *(walk + 1) == '*') {
                    walk++;

                    if (*walk == '*') {
                        binding_type = BT_LOOSE;
                    }
                }

                xcb_xrm_finalize_component(entry, &state);
                state.current_binding_type = binding_type;
                break;
            case '?':
                state.chunk = MAX(state.chunk, CS_COMPONENTS);
                if (state.chunk >= CS_PRE_VALUE_WHITESPACE) {
                    goto process_normally;
                }

                if (resource_only) {
                    goto done_error;
                }

                xcb_xrm_insert_component(entry, CT_WILDCARD, state.current_binding_type, NULL);
                break;
            case ' ':
            case '\t':
                /* Spaces are only allowed in the value, but spaces between the
                 * ':' and the value are omitted. */
                if (state.chunk <= CS_PRE_VALUE_WHITESPACE) {
                    break;
                }

                goto process_normally;
            case ':':
                if (resource_only) {
                    goto done_error;
                }

                if (state.chunk == CS_INITIAL) {
                    goto done_error;
                } else if (state.chunk == CS_COMPONENTS) {
                    xcb_xrm_finalize_component(entry, &state);
                    state.chunk = CS_PRE_VALUE_WHITESPACE;
                    break;
                } else if (state.chunk >= CS_PRE_VALUE_WHITESPACE) {
                    state.chunk = CS_VALUE;
                    goto process_normally;
                }
                break;
            default:
process_normally:
                state.chunk = MAX(state.chunk, CS_COMPONENTS);

                if (state.chunk == CS_PRE_VALUE_WHITESPACE) {
                    state.chunk = CS_VALUE;
                }

                if (state.chunk == CS_COMPONENTS) {
                    if ((*walk != '_' && *walk != '-') &&
                            (*walk < '0' || *walk > '9') &&
                            (*walk < 'a' || *walk > 'z') &&
                            (*walk < 'A' || *walk > 'Z')) {
                        goto done_error;
                    }
                }

                if (state.chunk < CS_VALUE) {
                    xcb_xrm_append_char(entry, &state, *walk);
                } else {
                    if (*walk == '\\') {
                        if (*(walk + 1) == ' ') {
                            *(value_walk++) = ' ';
                            walk++;
                        } else if (*(walk + 1) == '\t') {
                            *(value_walk++) = '\t';
                            walk++;
                        } else if (*(walk + 1) == '\\') {
                            *(value_walk++) = '\\';
                            walk++;
                        } else if (*(walk + 1) == 'n') {
                            *(value_walk++) = '\n';
                            walk++;
                        } else if (isdigit(*(walk + 1)) && isdigit(*(walk + 2)) && isdigit(*(walk + 3)) &&
                                *(walk + 1) < '8' && *(walk + 2) < '8' && *(walk + 3) < '8') {
                            *(value_walk++) = (*(walk + 1) - '0') * 64 + (*(walk + 2) - '0') * 8 + (*(walk + 3) - '0');
                            walk += 3;
                        } else {
                            *(value_walk++) = *walk;
                        }
                    } else {
                        *(value_walk++) = *walk;
                    }
                }

                break;
        }
    }

    if (state.chunk == CS_PRE_VALUE_WHITESPACE || state.chunk == CS_VALUE) {
        *value_walk = '\0';
        entry->value = strdup(value);
        if (entry->value == NULL)
            goto done_error;
    } else if (!resource_only) {
        /* Return error if there was no value for this entry. */
        goto done_error;
    } else {
        /* Since in the case of resource_only we never went into CS_VALUE, we
         * need to finalize the last component. */
        xcb_xrm_finalize_component(entry, &state);
    }

    /* Assert that this entry actually had a resource component. */
    if ((last = TAILQ_LAST(&(entry->components), components_head)) == NULL) {
        goto done_error;
    }

    /* Assert that the last component is not a wildcard. */
    if (last->type != CT_NORMAL) {
        goto done_error;
    }

    FREE(str);
    FREE(value);
    FREE(state.buffer);
    return 0;

done_error:
    FREE(str);
    FREE(value);
    FREE(state.buffer);

    xcb_xrm_entry_free(entry);
    *_entry = NULL;
    return -1;
}

/*
 * Returns the number of components of the given entry.
 *
 */
int __xcb_xrm_entry_num_components(xcb_xrm_entry_t *entry) {
    int result = 0;

    xcb_xrm_component_t *current;
    TAILQ_FOREACH(current, &(entry->components), components) {
        result++;
    }

    return result;
}

/*
 * Compares the two entries.
 * Returns 0 if they are the same and a negative error code otherwise.
 *
 */
int __xcb_xrm_entry_compare(xcb_xrm_entry_t *first, xcb_xrm_entry_t *second) {
    xcb_xrm_component_t *comp_first = TAILQ_FIRST(&(first->components));
    xcb_xrm_component_t *comp_second = TAILQ_FIRST(&(second->components));

    while (comp_first != NULL && comp_second != NULL) {
        if (comp_first->type != comp_second->type)
            return -FAILURE;

        if (comp_first->binding_type != comp_second->binding_type)
            return -FAILURE;

        if (comp_first->type == CT_NORMAL && strcmp(comp_first->name, comp_second->name) != 0)
            return -FAILURE;

        comp_first = TAILQ_NEXT(comp_first, components);
        comp_second = TAILQ_NEXT(comp_second, components);
    }

    /* At this point, at least one of the two is NULL. If they aren't both
     * NULL, they have a different number of components and cannot be equal. */
    if (comp_first != comp_second) {
        return -FAILURE;
    }

    return SUCCESS;
}

/*
 * Returns a string representation of this entry.
 *
 */
char *__xcb_xrm_entry_to_string(xcb_xrm_entry_t *entry) {
    char *result = NULL;
    char *value_buf;
    char *escaped_value;
    xcb_xrm_component_t *component;
    bool is_first = true;

    assert(entry != NULL);
    TAILQ_FOREACH(component, &(entry->components), components) {
        char *tmp;
        if (asprintf(&tmp, "%s%s%s", result == NULL ? "" : result,
                (is_first && component->binding_type == BT_TIGHT)
                    ? ""
                    : (component->binding_type == BT_TIGHT ? "." : "*"),
                component->type == CT_NORMAL ? component->name : "?") < 0) {
            FREE(result);
            return NULL;
        }
        FREE(result);
        result = tmp;

        is_first = false;
    }

    escaped_value = __xcb_xrm_entry_escape_value(entry->value);
    if (asprintf(&value_buf, "%s: %s", result, escaped_value) < 0) {
        FREE(escaped_value);
        FREE(result);
        return NULL;
    }
    FREE(escaped_value);
    FREE(result);
    result = value_buf;

    return result;
}

/*
 * Copy the entry.
 *
 */
xcb_xrm_entry_t *__xcb_xrm_entry_copy(xcb_xrm_entry_t *entry) {
    xcb_xrm_entry_t *copy;
    xcb_xrm_component_t *component;

    assert(entry != NULL);

    copy = calloc(1, sizeof(struct xcb_xrm_entry_t));
    if (copy == NULL)
        return NULL;

    copy->value = strdup(entry->value);
    if (copy->value == NULL) {
        FREE(copy);
        return NULL;
    }

    TAILQ_INIT(&(copy->components));
    TAILQ_FOREACH(component, &(entry->components), components) {
        xcb_xrm_component_t *new = calloc(1, sizeof(struct xcb_xrm_component_t));
        if (new == NULL) {
            xcb_xrm_entry_free(copy);
            return NULL;
        }

        new->name = strdup(component->name);
        if (new->name == NULL) {
            xcb_xrm_entry_free(copy);
            FREE(new);
            return NULL;
        }

        new->type = component->type;
        new->binding_type = component->binding_type;
        TAILQ_INSERT_TAIL(&(copy->components), new, components);
    }

    return copy;
}

/*
 * Escapes magic values.
 *
 */
char *__xcb_xrm_entry_escape_value(const char *value) {
    char *escaped;
    char *outwalk;
    int new_size = strlen(value) + 1;

    if (value[0] == ' ' || value[0] == '\t')
        new_size++;
    for (const char *walk = value; *walk != '\0'; walk++) {
        if (*walk == '\n' || *walk == '\\')
            new_size++;
    }

    escaped = calloc(1, new_size);
    if (escaped == NULL)
        return NULL;

    outwalk = escaped;
    if (value[0] == ' ' || value[0] == '\t') {
        *(outwalk++) = '\\';
    }
    for (const char *walk = value; *walk != '\0'; walk++) {
        if (*walk == '\n') {
            *(outwalk++) = '\\';
            *(outwalk++) = 'n';
        } else if (*walk == '\\') {
            *(outwalk++) = '\\';
            *(outwalk++) = '\\';
        } else {
            *(outwalk++) = *walk;
        }
    }
    *outwalk = '\0';

    return escaped;
}

/*
 * Frees the given entry.
 *
 * @param entry The entry to be freed.
 *
 */
void xcb_xrm_entry_free(xcb_xrm_entry_t *entry) {
    if (entry == NULL)
        return;

    FREE(entry->value);
    while (!TAILQ_EMPTY(&(entry->components))) {
        xcb_xrm_component_t *component = TAILQ_FIRST(&(entry->components));
        FREE(component->name);
        TAILQ_REMOVE(&(entry->components), component, components);
        FREE(component);
    }

    FREE(entry);
    return;
}
