#include <string.h>
#include <ctype.h>
#include "KinoSearch/Util/ToolSet.h"

#include "KinoSearch/Util/YAML.h"

/* Break up a string at newlines.  Discard blank lines.
 */
static VArray*
split_into_lines(const ByteBuf *input);
    
/* Count the number of leading spaces at the front of the ByteBuf's string. 
 */
static u32_t
calc_indent(ByteBuf *line);

/* Given an array of lines, create a 2-level array, with each array element
 * starting with a line at the lowest level of indentation.
 */
static VArray*
group_by_indent(VArray *lines);

/* Given an array of arrays, with each subarray being a group of lines, build
 * a complex data structure.
 */
static Obj*
build_parse_tree(VArray *groups);

/* Helper for build_parse_tree -- returns a VArray.
 */
static VArray*
build_array(VArray *groups);

/* Helper for build_parse_tree -- returns a Hash.
 */
static Hash*
build_hash(VArray *groups);

/* Attempt to extract a scalar value from an array line.  Return NULL if the
 * attempt fails.
 */
static ByteBuf*
array_line_extract(ByteBuf *line);

/* Extract a scalar hash key from a line.  Attempt to extract a value as well,
 * but store NULL in [val_ptr] if the attempt fails.
 */
static void
hash_line_extract(ByteBuf *line, ByteBuf **key_ptr, Obj **val_ptr);

/* Extract a scalar value, either an identifier delimited by whitespace or an
 * arbitrary value delimited by single quotes.  Fast forward past leading
 * spaces.  Advance [orig_ptr] past consumed character data.
 * 
 * Will return NULL if no scalar can be extracted between [orig_ptr] and
 * [limit], but will raise an error if provided with malformed input.
 */ 
static ByteBuf*
scalar_extract(char **orig_ptr, char **limit);

/* Unescape single quotes, decrementing the length of the ByteBuf as needed.
 */
static void
unescape(ByteBuf *input);

/* Dispatch to either encode_hash or encode_array.
 */
static void
encode_obj(Obj*obj, u32_t indent, ByteBuf *output);

/* Encode a Hash as YAML, descending recursively.
 */
static void
encode_hash(Hash *hash, u32_t indent, ByteBuf *output);

/* Encode a VArray as YAML, descending recursively.
 */
static void
encode_varray(VArray *varray, u32_t indent, ByteBuf *output);

/* Check for invalid scalar input (e.g. newlines), quote and escape if
 * necessary.
 */
static void
encode_scalar(ByteBuf *scalar, ByteBuf *output);

Obj*
YAML_parse_yaml(const ByteBuf *input)
{
    VArray *lines, *groups;
    Obj *output;

    lines = split_into_lines(input);
    if (lines->size == 0) {
        REFCOUNT_DEC(lines);
        return NULL;
    }

    groups = group_by_indent(lines);
    output = build_parse_tree(groups);

    REFCOUNT_DEC(lines);
    REFCOUNT_DEC(groups);

    return output;
}

ByteBuf*
YAML_encode_yaml(kino_Obj *obj) 
{
    ByteBuf *output = NULL;
    
    if (obj != NULL) {
        output = BB_new(0);
        encode_obj(obj, 0, output);
    }

    return output;
}

static VArray*
split_into_lines(const ByteBuf *input) 
{
    char *ptr, *start;
    char *limit = BBEND(input);
    VArray *out_array;
    u32_t linecount = 0;
    bool_t line_has_content = false;
    bool_t line_is_comment  = false;

    /* scan input for newlines to determine max size of out array */
    for (ptr = input->ptr; ptr < limit; ptr++) {
        if (*ptr == '\n')
            linecount++;
    }
    out_array = VA_new(linecount);

    for (ptr = input->ptr, start = input->ptr; ptr < limit; ptr++) {
        /* discard blank lines and comment only lines*/
        if (!isspace(*ptr)) {
            if (!line_has_content) {
                if (*ptr == '#') {
                    line_is_comment = true;
                }
            }
            line_has_content = true;
        }

        if (*ptr == '\n' || ptr == (limit - 1)) {
            if (line_has_content && !line_is_comment) {
                const size_t len = *ptr == '\n'
                    ? ptr - start
                    : ptr - start + 1;
                ByteBuf *line = BB_new_str(start, len);
                VA_Push(out_array, (Obj*)line);
                REFCOUNT_DEC(line);
            }
            start = ptr + 1;
            line_has_content = false;
            line_is_comment  = false;
        }
    }

    return out_array;
}

static u32_t
calc_indent(ByteBuf *input)
{
    char *ptr = input->ptr;
    char *limit = BBEND(input);
    u32_t indent = 0;

    while (ptr < limit && *ptr++ == ' ') { indent++; }

    return indent;
}

static VArray*
group_by_indent(VArray *lines)
{
    VArray *groups = VA_new(0);
    VArray *this_level = VA_new(0);
    u32_t starting_indent = 0;
    u32_t i;

    for (i = 0; i < lines->size; i++) {
        ByteBuf *line = (ByteBuf*)VA_Fetch(lines, i);
        if (this_level->size == 0) {
            starting_indent = calc_indent(line);
        }
        else if (calc_indent(line) == starting_indent) {
            VA_Push(groups, (Obj*)this_level);
            REFCOUNT_DEC(this_level);
            this_level = VA_new(0);
        }
        VA_Push(this_level, (Obj*)line);
    }

    if (this_level->size > 0) {
        VA_Push(groups, (Obj*)this_level);
    }
    REFCOUNT_DEC(this_level);

    return groups;
}


static Obj*
build_parse_tree(VArray *groups)
{
    VArray* first_group = (VArray*)VA_Fetch(groups, 0);
    ByteBuf* first_line = (ByteBuf*)VA_Fetch(first_group, 0);
    char *ptr = first_line->ptr;
    char *limit = BBEND(first_line);

    while (ptr < limit && *ptr == ' ') { ptr++; }

    if (*ptr == '-')
        return (Obj*)build_array(groups);
    else
        return (Obj*)build_hash(groups);
}

static VArray*
build_array(VArray *groups)
{
    VArray *out_array = VA_new(1);
    u32_t i;

    for (i = 0; i < groups->size; i++) {
        VArray *group = (VArray*)VA_Fetch(groups, i);

        /* if only one line, array element is a scalar */
        if (group->size == 1) {
            ByteBuf *line = (ByteBuf*)VA_Fetch(group, 0);
            ByteBuf *value = array_line_extract(line);

            /* don't accept empty values for array elements */
            if (value == NULL)
                CONFESS("Failed to extract array element: %s", line->ptr);

            VA_Push(out_array, (Obj*)value);
            REFCOUNT_DEC(value);
        }
        /* array element is a data structure */
        else {
            VArray *meaningful_lines;
            Obj    *inner_obj;

            /* discard first line */
            ByteBuf* first_line = (ByteBuf*)VA_Shift(group);
            REFCOUNT_DEC(first_line);

            meaningful_lines = group_by_indent(group);
            inner_obj = build_parse_tree(meaningful_lines);
            VA_Push(out_array, inner_obj);
            REFCOUNT_DEC(inner_obj);
            REFCOUNT_DEC(meaningful_lines);
        }
    }

    return out_array;
}

static Hash*
build_hash(VArray *groups)
{
    Hash *out_hash = Hash_new(16);
    u32_t i;

    for (i = 0; i < groups->size; i++) {
        VArray *group = (VArray*)VA_Fetch(groups, i);
        ByteBuf *first_line = (ByteBuf*)VA_Fetch(group, 0);
        ByteBuf *key = NULL;
        Obj     *val = NULL;

        /* attempt to extract a key value pair from the line */
        hash_line_extract(first_line, &key, &val);

        /* maybe the value is a complex data structure */
        if (val == NULL) {
            VArray *val_group, *sub_groups;
            u32_t j;

            if (group->size == 1) {
                CONFESS("Failed to extract hash value for %s", key->ptr);
            }
            val_group = VA_new(group->size - 1);

            /* copy all lines in the group except the first (the key) */
            for (j = 1; j < group->size; j++) {
                Obj *elem = VA_Fetch(group, j);
                Obj *copy = Obj_Clone(elem);
                VA_Push(val_group, copy);
                REFCOUNT_DEC(copy);
            }

            /* recurse */
            sub_groups = group_by_indent(val_group);
            val = build_parse_tree(sub_groups);

            REFCOUNT_DEC(sub_groups);
            REFCOUNT_DEC(val_group);
        }

        Hash_Store_BB(out_hash, key, val);
        REFCOUNT_DEC(key);
        REFCOUNT_DEC(val);
    }
    
    return out_hash;
}

static ByteBuf*
array_line_extract(ByteBuf *line)
{
    char *ptr   = line->ptr;
    char *limit = BBEND(line);

    /* scoot past leading whitespace and "- " */
    while (ptr < limit && *ptr == ' ') { ptr++; }
    if (ptr > (limit - 2) || strncmp(ptr, "- ", 2) != 0)
        return NULL;
    ptr += 2;

    return scalar_extract(&ptr, &limit);
}
static void
hash_line_extract(ByteBuf *line, ByteBuf **key_ptr, Obj **val_ptr)
{
    char *ptr   = line->ptr;
    char *limit = BBEND(line);

    /* extract key */
    *key_ptr = scalar_extract(&ptr, &limit);
    if (*key_ptr == NULL) {
        CONFESS("failed to extract key from %s", line->ptr);
    }
    while (*ptr == ' ' && ptr < (limit - 1)) { ptr++; }
    if (*ptr != ':')
        CONFESS("Malformed hash key line: %s", line->ptr);
    if (ptr == limit - 1)
        ptr += 1;
    else if (!isspace(*(ptr + 1)))
        CONFESS("Malformed hash key line: %s", line->ptr);
    else 
        ptr += 2;

    /* attempt to extract value (failure is ok) */
    *val_ptr = (Obj*)scalar_extract(&ptr, &limit);
}

ByteBuf*
scalar_extract(char **orig_ptr, char **limit_ptr) 
{
    char *ptr   = *orig_ptr;
    char *limit = *limit_ptr;
    char *start = ptr;
    ByteBuf *retval = NULL;

    /* blow past whitespace */
    while (ptr < limit && *ptr == ' ') { ptr++, start++; }

    /* blank */
    if (ptr == limit)
        ;
    /* if the value is single-quote delimited */
    else if (ptr + 1 < limit && *ptr == '\'') {
        /* don't include the opening quote */
        start++, ptr++;

        /* find the end, minus the closing quote */
        for ( ; ; ptr++) {
            if (ptr == limit)
                CONFESS("Malformed quotes: '%s'", *orig_ptr);

            if (*ptr == '\'') {
                if (ptr < (limit - 1) && *(ptr + 1) == '\'') {
                    /* advance past escaped single quote */
                    ptr += 1;
                }
                else {
                    /* bail out, we've found the closing quote */
                    break;
                }
            }
        }
        *orig_ptr = ptr + 1;
        retval = BB_new_str(start, (ptr - start));
        unescape(retval);
    }
    /*  if the value is space-delimited */
    else if (ptr < limit) {
        while (ptr < limit && !isspace(*ptr) && *ptr != ':') { ptr++; }
        *orig_ptr = ptr;
        retval = BB_new_str(start, ptr - start);
    }
    /* comment */
    else if (*ptr == '#') {
        return NULL;
    }
    else if (ptr < limit) {
        CONFESS("Illegal scalar value: %s", *orig_ptr);
    }

    return retval;
}

static void
unescape(ByteBuf *input) 
{
    char *source = input->ptr;
    char *dest   = source;
    char *const limit = BBEND(input);

    while (source < limit) {
        if (*source == '\'') {
            if (source < (limit - 1) && *(source + 1) == '\'')
                source++;
        }
        *dest++ = *source++;
    }
    input->len = dest - input->ptr;
    input->ptr[ input->len ] = '\0';
}

static void
encode_obj(Obj*obj, u32_t indent, ByteBuf *output)
{
    if (OBJ_IS_A(obj, HASH)) {
        encode_hash((Hash*)obj, indent, output);
    }
    else if (OBJ_IS_A(obj, VARRAY)) {
        encode_varray((VArray*)obj, indent, output);
    }
    else {
        CONFESS("Object is neither Hash nor VArray: %s", obj->_->class_name);
    }
}

static void
append_spaces(ByteBuf *output, u32_t num_spaces)
{
    BB_GROW(output, output->len + num_spaces);
    memset(BBEND(output), ' ', num_spaces);
    output->len += num_spaces;
    *BBEND(output) = '\0';
}

static void
encode_hash(Hash *hash, u32_t indent, ByteBuf *output)
{
    kino_ByteBuf *key;
    kino_Obj     *val;

    /* ballpark extra space to minimize reallocations */
    BB_GROW(output, output->len + (hash->size * (indent + 5)));

    Hash_Iter_Init(hash);
    while (Hash_Iter_Next(hash, &key, &val)) {
        /* append the key to the output */
        BB_GROW(output, output->len + indent + key->len + 20);
        append_spaces(output, indent);
        encode_scalar(key, output);
        BB_Cat_Str(output, ": ", 2);

        if (OBJ_IS_A(val, BYTEBUF)) {
            encode_scalar((ByteBuf*)val, output);
            BB_Cat_Str(output, "\n", 1);
        }
        else {
            BB_Cat_Str(output, "\n", 1);
            encode_obj(val, indent + 2, output);
        }
    }
}

static void
encode_scalar(ByteBuf *scalar, ByteBuf *output)
{
    char *start = scalar->ptr;
    char *limit = BBEND(scalar);
    char *source;
    size_t space = 0;
    bool_t needs_quoting = false;

    /* chop whitespace from both ends */
    for ( ; limit > start; limit--) {
        if (!isspace(*(limit - 1)))
            break;
    }
    for ( ; start < limit; start++) {
        if (!isspace(*start))
            break;
    }

    /* validate input, see if quotes are required */
    for (source = start; source < limit; source++) {
        if (!isprint(*source))
            CONFESS("invalid character: %d", *source);

        switch (*source) {
        case '\r':
        case '\n':
        case '\t': 
            CONFESS("tab or return character in input: %s", scalar->ptr);
            break;
        
        case '\'':
            space++;
            /* fall through */
        case ':':
        case ' ':
            needs_quoting = true;
            space++;
            break;

        default:
            space++;
            break;
        }
    }

    /* quote empty strings */
    if (space == 0)
        needs_quoting = true;

    BB_GROW(output, output->len + space + 3);
    if (needs_quoting) {
        char *dest = BBEND(output);
        space += 2;

        *dest++ = '\'';
        for (source = start; source < limit; source++) {
            if (*source == '\'')
                *dest++ = '\'';
            *dest++ = *source;
        }
        *dest++ = '\'';
    }
    else {
        memcpy(BBEND(output), start, space);
    }
    output->len += space;
    output->ptr[ output->len ] = '\0';
}

static void
encode_varray(VArray *varray, u32_t indent, ByteBuf *output)
{
    u32_t i;

    /* ballpark space */
    BB_GROW(output, output->len + (varray->size * (indent + 5)));

    for (i = 0; i < varray->size; i++) {
        kino_Obj *elem = VA_Fetch(varray, i);

        BB_GROW(output, output->len + indent + 5);
        append_spaces(output, indent);
        BB_Cat_Str(output, "- ", 2);

        if (elem == NULL) {
            CONFESS("missing element in varray");
        }
        else if (OBJ_IS_A(elem, BYTEBUF)) {
            encode_scalar((ByteBuf*)elem, output);
            BB_Cat_Str(output, "\n", 1);
        }
        else {
            BB_Cat_Str(output, "\n", 1);
            encode_obj(elem, indent + 2, output);
        }
    }
}

/* Copyright 2006-2007 Marvin Humphrey
 *
 * This program is free software; you can redistribute it and/or modify
 * under the same terms as Perl itself.
 */