// STRING_BLOCKS :: https://github.com/prideout/par
// String extraction and concatenation, especially useful for snippets of GLSL or Lua.
//
// This little library extracts blocks of text from a memory blob or file, then lets you retrieve
// them by name. It also makes it easy to glue together a sequence of blocks.
//
// Each block of text is assigned a name using a prefix line that starts with three dash characters,
// such as "--- the_name" or "--- my.block".
//
// For example, suppose you have a file called "shaders.glsl" that looks like this:
//
//    --- my_shader
//    void main() { ... }
//    --- common
//    uniform vec4 resolution;
//    uniform vec4 color;
//
// You can use this library to read in the file and extract one of the blocks:
//
//     parsb_context* blocks = parsb_create_context((parsb_options){});
//     parsb_add_blocks_from_file(blocks, "shaders.glsl");
//     const char* single = parsb_get_blocks(blocks, "my_shader");
//
// You can also concatenate blocks using a space-delimited list of names:
//
//     const char* concatenated = parsb_get_blocks(blocks, "common my_shader");
//
// You can also add or replace blocks on the fly:
//
//     parsb_add_block(blocks, "prefix", "#version 330\n");
//     const char* concatenated = parsb_get_blocks(blocks, "prefix common my_shader");
//
// The "blocks" context in the above examples holds a cache of generated strings, so be sure to
// destroy it when you're done:
//
//     parsb_destroy_context(blocks);
//
// Distributed under the MIT License, see bottom of file.

#ifndef PAR_STRING_BLOCKS_H
#define PAR_STRING_BLOCKS_H

#ifdef __cplusplus
extern "C" {
#endif

#include <stdbool.h>

// OPTIONS
// -------
// line_directives ... adds #line annotations into concatenated strings for better error messages.
typedef struct parsb_options {
    bool line_directives;
} parsb_options;

// CONTEXT CREATION AND DESTRUCTION
// --------------------------------
// A context is an opaque handle to a memory arena. All generated strings are owned by the context
// and freed when the context is destroyed.
typedef struct parsb_context_s parsb_context;
parsb_context* parsb_create_context(parsb_options);
void parsb_destroy_context(parsb_context*);

// ADDING AND REPLACING BLOCKS
// ---------------------------
// When using the plural form (add_blocks), the submitted buffer may contain multiple blocks, each
// with a name defined by its closest preceding triple-dash line. If a block with the specified name
// already exists, it gets replaced.
//
// The singular form (add_block) adds a single block whose name is explicitly specified as an
// argument. Again, if a block with the given name already exists, it gets replaced.
//
// These functions do not retain the passed-in strings so clients can free them after pushing them.
void parsb_add_blocks(parsb_context*, const char* buffer, int buffer_size);
void parsb_add_block(parsb_context*, const char* name, const char* body);
#ifndef PARSB_NO_STDIO
void parsb_add_blocks_from_file(parsb_context* context, const char* filename);
#endif

// EXTRACTING AND CONCATENATING BLOCKS
// -----------------------------------
// The block_names string is a space-separated list of block names that are being requested. The
// returned string is owned by the context, so please make a copy if you need it to outlive the
// context. If the returned string is null, then one or more of the block names could not be found.
const char* parsb_get_blocks(parsb_context*, const char* block_names);

// GETTING BLOCKS BY INDEX
// -----------------------
int parsb_get_num_blocks(const parsb_context*);
void parsb_get_block(const parsb_context*, int index, const char** name, const char** body);

// SAVING THE BLOCK LIST
// ---------------------
// These functions export the entire "database" of atomic blocks.
typedef void (*parsb_write_line)(const char* line, void* userdata);
void parsb_write_blocks(parsb_context*, parsb_write_line writefn, void* user);
#ifndef PARSB_NO_STDIO
void parsb_write_blocks_to_file(parsb_context*, const char* filename);
#endif

#ifndef PARSB_MAX_NUM_BLOCKS
#define PARSB_MAX_NUM_BLOCKS 128
#endif

#ifndef PARSB_MAX_NAME_LENGTH
#define PARSB_MAX_NAME_LENGTH 256
#endif

#ifndef PARSB_MAX_LINE_LENGTH
#define PARSB_MAX_LINE_LENGTH 256
#endif

#ifdef __cplusplus
}
#endif

// -----------------------------------------------------------------------------
// END PUBLIC API
// -----------------------------------------------------------------------------

#ifdef PAR_STRING_BLOCKS_IMPLEMENTATION

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

#ifndef PARSB_NO_STDIO
#include <stdio.h>
#endif

#define PARSB_MIN(a, b) (a > b ? b : a) 

typedef struct {
    int count;
    char* values[PARSB_MAX_NUM_BLOCKS];
    char* names[PARSB_MAX_NUM_BLOCKS];
} parsb__list;

struct parsb_context_s {
    parsb_options options;
    parsb__list blocks;
    parsb__list results;
};

static char* parsb__add_or_replace(parsb_context*, const char* id, const char* value,
    int value_size, int line_number);
static char* parsb__list_add(parsb__list*, const char* id, const char* value, int value_size,
    int line_number);
static char* parsb__list_get(parsb__list*, const char* id, int idlen);
static void parsb__list_free(parsb__list* );

parsb_context* parsb_create_context(parsb_options options) {
    parsb_context* context = (parsb_context*) calloc(1, sizeof(parsb_context));
    context->options = options;
    return context;
}

void parsb_destroy_context(parsb_context* context) {
    parsb__list_free(&context->blocks);
    parsb__list_free(&context->results);
    free(context);
}

void parsb_add_blocks(parsb_context* context, const char* blob, int buffer_size) {
    const char* previous_block = 0;
    char previous_name[PARSB_MAX_NAME_LENGTH];
    int line_number = 0;
    int block_line_number = 0;
    for (int i = 0; i < buffer_size - 4; i++) {
        if (blob[i] != '-' || blob[i + 1] != '-' || blob[i + 2] != '-' || blob[i + 3] != ' ') {
            if (blob[i] == '\n') {
                line_number++;
            }
            continue;
        }
        if (previous_block) {
            parsb__add_or_replace(context, previous_name, previous_block,
                i - (previous_block - blob), block_line_number);
        }
        i += 4;
        const char* name = blob + i;
        const char* block_start = 0;
        for (; i < buffer_size; i++) {
            if (blob[i] == '\n') {
                line_number++;
                int name_length = i - (name - blob);
                memcpy(previous_name, name, name_length);
                block_line_number = line_number + 2;
                previous_name[name_length] = 0;
                block_start = blob + i + 1;
                break;
            }
            if (isspace(blob[i])) {
                int name_length = i - (name - blob);
                memcpy(previous_name, name, name_length);
                block_line_number = line_number + 2;
                previous_name[name_length] = 0;
                for (i++; i < buffer_size; i++) {
                    if (blob[i] == '\n') {
                        line_number++;
                        block_start = blob + i + 1;
                        break;
                    }
                }
                break;
            }
        }
        if (block_start == 0) {
            return;
        }
        previous_block = block_start;
    }
    if (previous_block) {
        parsb__add_or_replace(context, previous_name, previous_block,
            buffer_size - (previous_block - blob), block_line_number);
    }
}

void parsb_add_block(parsb_context* context, const char* name, const char* body) {
    char* dup = strdup(body);
    parsb__add_or_replace(context, name, dup, 1 + strlen(body), 0);
}

const char* parsb_get_blocks(parsb_context* context, const char* block_names) {
    int len = strlen(block_names);
    const char* name = block_names;
    int name_length = 0;
    int result_length = 0;

    // First pass determines the amount of required memory.
    int num_names = 0;
    for (int i = 0; i < len; i++) {
        char c = block_names[i];
        if (isspace(c) || !c) {
            const char* block = parsb__list_get(&context->blocks, name, name_length);
            if (block) {
                result_length += strlen(block);
                num_names++;
            } else {
                return NULL;
            }
            name_length = 0;
            name = block_names + i + 1;
        } else {
            name_length++;
        }
    }
    const char* block = parsb__list_get(&context->blocks, name, name_length);
    if (block) {
        result_length += strlen(block);
        num_names++;
    }

    // If no concatenation is required, return early.
    if (num_names == 1) {
        return parsb__list_get(&context->blocks, name, name_length);
    }

    // Allocate storage for the result.
    char* result = parsb__list_add(&context->results, 0, 0, result_length, 0);
    char* cursor = result;

    // Second pass populates the result.
    name = block_names;
    name_length = 0;
    for (int i = 0; i < len; i++) {
        char c = block_names[i];
        if (isspace(c) || !c) {
            const char* block = parsb__list_get(&context->blocks, name, name_length);
            if (block) {
                memcpy(cursor, block, strlen(block));
                cursor += strlen(block);
            }
            name_length = 0;
            name = block_names + i + 1;
        } else {
            name_length++;
        }
    }
    block = parsb__list_get(&context->blocks, name, name_length);
    if (block) {
        memcpy(cursor, block, strlen(block));
        cursor += strlen(block);
    }
    return result;
}

int parsb_get_num_blocks(const parsb_context* context) {
    return context->blocks.count;
}

void parsb_get_block(const parsb_context* context, int index, const char** name,
    const char** body) {
    if (index < 0 || index >= context->blocks.count) {
        return;
    }
    *name = context->blocks.names[index];
    *body = context->blocks.values[index];
}

void parsb_write_blocks(parsb_context* context, parsb_write_line writefn, void* userdata) {
    char line[PARSB_MAX_LINE_LENGTH + 1] = {0};
    for (int i = 0; i < context->blocks.count; i++) {

        sprintf(line, "--- %s", context->blocks.names[i]);
        writefn(line, userdata);

        const char* cursor = context->blocks.values[i];
        const int blocklen = strlen(cursor);
        int previous = 0;
        for (int i = 0; i < blocklen; i++) {
            if (cursor[i] == '\n') {
                int line_length = PARSB_MIN(i - previous, PARSB_MAX_LINE_LENGTH);
                memcpy(line, cursor + previous, line_length);
                line[line_length] = 0;
                writefn(line, userdata);
                previous = i + 1;
            } else if (i == blocklen - 1) {
                int line_length = PARSB_MIN(1 + i - previous, PARSB_MAX_LINE_LENGTH);
                memcpy(line, cursor + previous, line_length);
                line[line_length] = 0;
                writefn(line, userdata);
                previous = i + 1;
            }
        }
    }
}

static char* parsb__add_or_replace(parsb_context* context, const char* id, const char* value,
    int value_size, int line_number) {
    line_number = context->options.line_directives ? line_number : 0;
    const size_t idlen = strlen(id);
    for (int i = 0; i < context->blocks.count; i++) {
        if (strncmp(id, context->blocks.names[i], idlen) == 0) {
            free(context->blocks.values[i]);
            context->blocks.values[i] = strndup(value, value_size);
            return context->blocks.values[i];
        }
    }
    return parsb__list_add(&context->blocks, id, value, value_size, line_number);
}

static char* parsb__list_add(parsb__list* list, const char* name,
    const char* value, int value_size, int line_number) {
    if (value_size == 0) {
        return NULL;
    }

    if (list->count == PARSB_MAX_NUM_BLOCKS) {
        assert(false && "Please increase PARSB_MAX_NUM_BLOCKS.");
        return NULL;
    }

    char* storage;
    char* cursor;

    if (line_number > 0) {
        char line_directive[16] = {0};
        int prefix_length = snprintf(line_directive, 16, "\n#line %d\n", line_number);
        storage = (char*) calloc(1, prefix_length + value_size + 1);
        memcpy(storage, line_directive, prefix_length);
        cursor = storage + prefix_length;
    } else {
        storage = cursor = (char*) calloc(1, value_size + 1);
    }

    if (value) {
        memcpy(cursor, value, value_size);
    }

    #if PARSB_ENABLE_TRIM
    value_size--;
    while (isspace(cursor[value_size])) {
        cursor[value_size] = 0;
        value_size--;
        if (value_size == 0) {
            break;
        }
    }
    #endif

    if (name) {
        list->names[list->count] = strdup(name);
    } else {
        list->names[list->count] = 0;
    }

    list->values[list->count] = storage;
    list->count++;
    return storage;
}

static char* parsb__list_get(parsb__list* list, const char* name, int idlen) {
    for (int i = 0; i < list->count; i++) {
        if (strncmp(name, list->names[i], idlen) == 0) {
            return list->values[i];
        }
    }
    return NULL;
}

static void parsb__list_free(parsb__list* list) {
    for (int i = 0; i < list->count; i++) {
        free(list->names[i]);
        free(list->values[i]);
    }
    list->count = 0;
}

#ifndef PARSB_NO_STDIO

void parsb_add_blocks_from_file(parsb_context* context, const char* filename) {
    FILE* f = fopen(filename, "r");
    if (!f) {
        fprintf(stderr, "Unable to open %s\n", filename);
        return;
    }
    fseek(f, 0, SEEK_END);
    int length = ftell(f);
    fseek(f, 0, SEEK_SET);
    char* buffer = (char*) malloc(length);
    fread(buffer, 1, length, f);
    fclose(f);
    parsb_add_blocks(context, buffer, length);
    free(buffer);
}

static void writefn(const char* line, void* userdata) {
    fprintf((FILE*) userdata, "%s\n", line);
}

void parsb_write_blocks_to_file(parsb_context* context, const char* filename) {
    FILE* f = fopen(filename, "w");
    if (!f) {
        fprintf(stderr, "Unable to open %s\n", filename);
        return;
    }
    parsb_write_blocks(context, writefn, f);
    fclose(f);
}

#endif

#endif // PAR_STRING_BLOCKS_IMPLEMENTATION
#endif // PAR_STRING_BLOCKS_H

// par_string_blocks is distributed under the MIT license:
//
// Copyright (c) 2020 Philip Rideout
//
// 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 OR COPYRIGHT HOLDERS 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.
