// =============================================================== //
//                                                                 //
//   File      : adstring.cxx                                      //
//   Purpose   : various string functions                          //
//                                                                 //
//   Institute of Microbiology (Technical University Munich)       //
//   http://www.arb-home.de/                                       //
//                                                                 //
// =============================================================== //

#include <arb_backtrace.h>
#include <arb_strbuf.h>
#include <arb_defs.h>
#include <arb_str.h>

#include "gb_key.h"
#include "gb_aci.h"

#include <SigHandler.h>

#include <execinfo.h>

#include <cstdarg>
#include <cctype>
#include <cerrno>
#include <ctime>
#include <setjmp.h>

#include <valgrind.h>

static char *GBS_string_2_key_with_exclusions(const char *str, const char *additional) {
    // converts any string to a valid key (all chars in 'additional' are additionally allowed)
    char buf[GB_KEY_LEN_MAX+1];
    int i;
    int c;
    for (i=0; i<GB_KEY_LEN_MAX;) {
        c = *(str++);
        if (!c) break;

        if (c==' ' || c == '_') {
            buf[i++]  = '_';
        }
        else if (isalnum(c) || strchr(additional, c)) {
            buf[i++]  = c;
        }
    }
    for (; i<GB_KEY_LEN_MIN; i++) buf[i] = '_';
    buf[i] = 0;
    return ARB_strdup(buf);
}

char *GBS_string_2_key(const char *str) { // converts any string to a valid key
    return GBS_string_2_key_with_exclusions(str, "");
}

char *GB_memdup(const char *source, size_t len) {
    char *dest = ARB_alloc<char>(len);
    memcpy(dest, source, len);
    return dest;
}

static const char *EMPTY_KEY_NOT_ALLOWED = "Empty key is not allowed";

inline __ATTR__USERESULT GB_ERROR check_key(const char *key, int len) {
    // test if 'key' is a valid non-hierarchical database key.
    // i.e. contains only letters, numbers and '_' and
    // is inside length constraints GB_KEY_LEN_MIN/GB_KEY_LEN_MAX.

    if (len < GB_KEY_LEN_MIN) {
        if (!len) return EMPTY_KEY_NOT_ALLOWED;
        return GBS_global_string("Invalid key '%s': too short", key);
    }
    if (len > GB_KEY_LEN_MAX) return GBS_global_string("Invalid key '%s': too long", key);

    for (int i = 0; i<len; ++i) {
        char c         = key[i];
        bool validChar = isalnum(c) || c == '_';
        if (!validChar) {
            return GBS_global_string("Invalid character '%c' in '%s'; allowed: a-z A-Z 0-9 '_' ", c, key);
        }
    }

    return NULp;
}
GB_ERROR GB_check_key(const char *key) { // goes to header: __ATTR__USERESULT
    // test if 'key' is a valid non-hierarchical database key
    // (i.e. a valid name for a container or field).

    return check_key(key, key ? strlen(key) : 0);
}

GB_ERROR GB_check_hkey(const char *key) { // goes to header: __ATTR__USERESULT
    // test whether 'key' is a hierarchical key,
    // i.e. consists of subkeys (accepted by GB_check_key), separated by '/'.

    GB_ERROR err = NULp;

    if (key && key[0] == '/') ++key; // accept + remove leading '/'
    if (!key || !key[0]) err = EMPTY_KEY_NOT_ALLOWED; // reject NULp, empty (or single slash)

    while (!err && key[0]) {
        int nonSlashPart = strcspn(key, "/");

        err = check_key(key, nonSlashPart);
        if (!err) {
            key += nonSlashPart;
            if (key[0] == '/') {
                ++key;
                if (key[0] == 0) { // nothing after slash
                    err = EMPTY_KEY_NOT_ALLOWED;
                }
            }
            else {
                gb_assert(key[0] == 0);
            }
        }
    }
    return err;
}

// ----------------------------------------------
//      escape/unescape characters in strings

char *GBS_escape_string(const char *str, const char *chars_to_escape, char escape_char) {
    /*! escape characters in 'str'
     *
     * uses a special escape-method, which eliminates all 'chars_to_escape' completely
     * from str (this makes further processing of the string more easy)
     *
     * @param str string to escape
     *
     * @param escape_char is the character used for escaping. For performance reasons it
     * should be a character rarely used in 'str'.
     *
     * @param chars_to_escape may not contain 'A'-'Z' (these are used for escaping)
     * and it may not be longer than 26 bytes
     *
     * @return heap copy of escaped string
     *
     * Inverse of GBS_unescape_string()
     */

    int   len    = strlen(str);
    char *buffer = ARB_alloc<char>(2*len+1);
    int   j      = 0;
    int   i;

    gb_assert(strlen(chars_to_escape) <= 26);
    gb_assert(!strchr(chars_to_escape, escape_char)); // escape_char may not be included in chars_to_escape

    for (i = 0; str[i]; ++i) {
        if (str[i] == escape_char) {
            buffer[j++] = escape_char;
            buffer[j++] = escape_char;
        }
        else {
            const char *found = strchr(chars_to_escape, str[i]);
            if (found) {
                buffer[j++] = escape_char;
                buffer[j++] = (found-chars_to_escape+'A');

                gb_assert(found[0]<'A' || found[0]>'Z'); // illegal character in chars_to_escape
            }
            else {

                buffer[j++] = str[i];
            }
        }
    }
    buffer[j] = 0;

    return buffer;
}

char *GBS_unescape_string(const char *str, const char *escaped_chars, char escape_char) {
    //! inverse of GB_escape_string() - for params see there

    int   len    = strlen(str);
    char *buffer = ARB_alloc<char>(len+1);
    int   j      = 0;
    int   i;

#if defined(ASSERTION_USED)
    int escaped_chars_len = strlen(escaped_chars);
#endif // ASSERTION_USED

    gb_assert(strlen(escaped_chars) <= 26);
    gb_assert(!strchr(escaped_chars, escape_char)); // escape_char may not be included in chars_to_escape

    for (i = 0; str[i]; ++i) {
        if (str[i] == escape_char) {
            if (str[i+1] == escape_char) {
                buffer[j++] = escape_char;
            }
            else {
                int idx = str[i+1]-'A';

                gb_assert(idx >= 0 && idx<escaped_chars_len);
                buffer[j++] = escaped_chars[idx];
            }
            ++i;
        }
        else {
            buffer[j++] = str[i];
        }
    }
    buffer[j] = 0;

    return buffer;
}

char *GBS_eval_env(GB_CSTR p) {
    GB_ERROR      error = NULp;
    GB_CSTR       ka;
    GBS_strstruct out(1000);

    while ((ka = GBS_find_string(p, "$(", 0))) {
        GB_CSTR kz = strchr(ka, ')');
        if (!kz) {
            error = GBS_global_string("missing ')' for envvar '%s'", p);
            break;
        }
        else {
            char *envvar = ARB_strpartdup(ka+2, kz-1);
            int   len    = ka-p;

            if (len) out.ncat(p, len);

            GB_CSTR genv = GB_getenv(envvar);
            if (genv) out.cat(genv);

            p = kz+1;
            free(envvar);
        }
    }

    if (error) {
        GB_export_error(error);
        return NULp;
    }

    out.cat(p); // copy rest
    return out.release_memfriendly();
}

long GBS_gcgchecksum(const char *seq) {
    // GCGchecksum
    long i;
    long check  = 0;
    long count  = 0;
    long seqlen = strlen(seq);

    for (i = 0; i < seqlen; i++) {
        count++;
        check += count * toupper(seq[i]);
        if (count == 57) count = 0;
    }
    check %= 10000;

    return check;
}

// Table of CRC-32's of all single byte values (made by makecrc.c of ZIP source)
uint32_t crctab[] = {
    0x00000000L, 0x77073096L, 0xee0e612cL, 0x990951baL, 0x076dc419L,
    0x706af48fL, 0xe963a535L, 0x9e6495a3L, 0x0edb8832L, 0x79dcb8a4L,
    0xe0d5e91eL, 0x97d2d988L, 0x09b64c2bL, 0x7eb17cbdL, 0xe7b82d07L,
    0x90bf1d91L, 0x1db71064L, 0x6ab020f2L, 0xf3b97148L, 0x84be41deL,
    0x1adad47dL, 0x6ddde4ebL, 0xf4d4b551L, 0x83d385c7L, 0x136c9856L,
    0x646ba8c0L, 0xfd62f97aL, 0x8a65c9ecL, 0x14015c4fL, 0x63066cd9L,
    0xfa0f3d63L, 0x8d080df5L, 0x3b6e20c8L, 0x4c69105eL, 0xd56041e4L,
    0xa2677172L, 0x3c03e4d1L, 0x4b04d447L, 0xd20d85fdL, 0xa50ab56bL,
    0x35b5a8faL, 0x42b2986cL, 0xdbbbc9d6L, 0xacbcf940L, 0x32d86ce3L,
    0x45df5c75L, 0xdcd60dcfL, 0xabd13d59L, 0x26d930acL, 0x51de003aL,
    0xc8d75180L, 0xbfd06116L, 0x21b4f4b5L, 0x56b3c423L, 0xcfba9599L,
    0xb8bda50fL, 0x2802b89eL, 0x5f058808L, 0xc60cd9b2L, 0xb10be924L,
    0x2f6f7c87L, 0x58684c11L, 0xc1611dabL, 0xb6662d3dL, 0x76dc4190L,
    0x01db7106L, 0x98d220bcL, 0xefd5102aL, 0x71b18589L, 0x06b6b51fL,
    0x9fbfe4a5L, 0xe8b8d433L, 0x7807c9a2L, 0x0f00f934L, 0x9609a88eL,
    0xe10e9818L, 0x7f6a0dbbL, 0x086d3d2dL, 0x91646c97L, 0xe6635c01L,
    0x6b6b51f4L, 0x1c6c6162L, 0x856530d8L, 0xf262004eL, 0x6c0695edL,
    0x1b01a57bL, 0x8208f4c1L, 0xf50fc457L, 0x65b0d9c6L, 0x12b7e950L,
    0x8bbeb8eaL, 0xfcb9887cL, 0x62dd1ddfL, 0x15da2d49L, 0x8cd37cf3L,
    0xfbd44c65L, 0x4db26158L, 0x3ab551ceL, 0xa3bc0074L, 0xd4bb30e2L,
    0x4adfa541L, 0x3dd895d7L, 0xa4d1c46dL, 0xd3d6f4fbL, 0x4369e96aL,
    0x346ed9fcL, 0xad678846L, 0xda60b8d0L, 0x44042d73L, 0x33031de5L,
    0xaa0a4c5fL, 0xdd0d7cc9L, 0x5005713cL, 0x270241aaL, 0xbe0b1010L,
    0xc90c2086L, 0x5768b525L, 0x206f85b3L, 0xb966d409L, 0xce61e49fL,
    0x5edef90eL, 0x29d9c998L, 0xb0d09822L, 0xc7d7a8b4L, 0x59b33d17L,
    0x2eb40d81L, 0xb7bd5c3bL, 0xc0ba6cadL, 0xedb88320L, 0x9abfb3b6L,
    0x03b6e20cL, 0x74b1d29aL, 0xead54739L, 0x9dd277afL, 0x04db2615L,
    0x73dc1683L, 0xe3630b12L, 0x94643b84L, 0x0d6d6a3eL, 0x7a6a5aa8L,
    0xe40ecf0bL, 0x9309ff9dL, 0x0a00ae27L, 0x7d079eb1L, 0xf00f9344L,
    0x8708a3d2L, 0x1e01f268L, 0x6906c2feL, 0xf762575dL, 0x806567cbL,
    0x196c3671L, 0x6e6b06e7L, 0xfed41b76L, 0x89d32be0L, 0x10da7a5aL,
    0x67dd4accL, 0xf9b9df6fL, 0x8ebeeff9L, 0x17b7be43L, 0x60b08ed5L,
    0xd6d6a3e8L, 0xa1d1937eL, 0x38d8c2c4L, 0x4fdff252L, 0xd1bb67f1L,
    0xa6bc5767L, 0x3fb506ddL, 0x48b2364bL, 0xd80d2bdaL, 0xaf0a1b4cL,
    0x36034af6L, 0x41047a60L, 0xdf60efc3L, 0xa867df55L, 0x316e8eefL,
    0x4669be79L, 0xcb61b38cL, 0xbc66831aL, 0x256fd2a0L, 0x5268e236L,
    0xcc0c7795L, 0xbb0b4703L, 0x220216b9L, 0x5505262fL, 0xc5ba3bbeL,
    0xb2bd0b28L, 0x2bb45a92L, 0x5cb36a04L, 0xc2d7ffa7L, 0xb5d0cf31L,
    0x2cd99e8bL, 0x5bdeae1dL, 0x9b64c2b0L, 0xec63f226L, 0x756aa39cL,
    0x026d930aL, 0x9c0906a9L, 0xeb0e363fL, 0x72076785L, 0x05005713L,
    0x95bf4a82L, 0xe2b87a14L, 0x7bb12baeL, 0x0cb61b38L, 0x92d28e9bL,
    0xe5d5be0dL, 0x7cdcefb7L, 0x0bdbdf21L, 0x86d3d2d4L, 0xf1d4e242L,
    0x68ddb3f8L, 0x1fda836eL, 0x81be16cdL, 0xf6b9265bL, 0x6fb077e1L,
    0x18b74777L, 0x88085ae6L, 0xff0f6a70L, 0x66063bcaL, 0x11010b5cL,
    0x8f659effL, 0xf862ae69L, 0x616bffd3L, 0x166ccf45L, 0xa00ae278L,
    0xd70dd2eeL, 0x4e048354L, 0x3903b3c2L, 0xa7672661L, 0xd06016f7L,
    0x4969474dL, 0x3e6e77dbL, 0xaed16a4aL, 0xd9d65adcL, 0x40df0b66L,
    0x37d83bf0L, 0xa9bcae53L, 0xdebb9ec5L, 0x47b2cf7fL, 0x30b5ffe9L,
    0xbdbdf21cL, 0xcabac28aL, 0x53b39330L, 0x24b4a3a6L, 0xbad03605L,
    0xcdd70693L, 0x54de5729L, 0x23d967bfL, 0xb3667a2eL, 0xc4614ab8L,
    0x5d681b02L, 0x2a6f2b94L, 0xb40bbe37L, 0xc30c8ea1L, 0x5a05df1bL,
    0x2d02ef8dL
};

uint32_t GB_checksum(const char *seq, long length, int ignore_case, const char *exclude) {
    /* CRC32checksum: modified from CRC-32 algorithm found in ZIP compression source
     * if ignore_case == true -> treat all characters as uppercase-chars (applies to exclude too)
     */

    unsigned long c = 0xffffffffL;
    long          n = length;
    int           i;
    int           tab[256]; // @@@ avoid recalc for each call

    for (i=0; i<256; i++) { // LOOP_VECTORIZED // tested down to gcc 5.5.0 (may fail on older gcc versions)
        tab[i] = ignore_case ? toupper(i) : i;
    }

    if (exclude) {
        while (1) {
            int k  = *(unsigned char *)exclude++;
            if (!k) break;
            tab[k] = 0;
            if (ignore_case) tab[toupper(k)] = tab[tolower(k)] = 0;
        }
    }

    while (n--) {
        i = tab[*(const unsigned char *)seq++];
        if (i) {
            c = crctab[((int) c ^ i) & 0xff] ^ (c >> 8);
        }
    }
    c = c ^ 0xffffffffL;
    return c;
}

uint32_t GBS_checksum(const char *seq, int ignore_case, const char *exclude) {
     // if 'ignore_case' == true -> treat all characters as uppercase-chars (applies to 'exclude' too)
    return GB_checksum(seq, strlen(seq), ignore_case, exclude);
}

size_t GBS_shorten_repeated_data(char *data) {
    // shortens repeats in 'data'
    // This function modifies 'data'!!
    // e.g. "..............................ACGT....................TGCA"
    // ->   ".{30}ACGT.{20}TGCA"

#if defined(DEBUG)
    size_t  orgLen    = strlen(data);
#endif // DEBUG
    char   *dataStart = data;
    char   *dest      = data;
    size_t  repeat    = 1;
    char    last      = *data++;

    while (last) {
        char curr = *data++;
        if (curr == last) {
            repeat++;
        }
        else {
            if (repeat >= 5) {
                dest += sprintf(dest, "%c{%zu}", last, repeat); // insert repeat count
            }
            else {
                size_t r;
                for (r = 0; r<repeat; r++) *dest++ = last; // insert plain
            }
            last   = curr;
            repeat = 1;
        }
    }

    *dest = 0;

#if defined(DEBUG)

    gb_assert(strlen(dataStart) <= orgLen);
#endif // DEBUG
    return dest-dataStart;
}


// ------------------------------------------
//      helper classes for tagged fields

class TextRef {
    const char *data; // has no terminal zero-byte!
    int         length;

public:
    TextRef() : data(NULp), length(-1) {}
    TextRef(const char *data_, int length_) : data(data_), length(length_) {}
    explicit TextRef(const char *zeroTerminated) : data(zeroTerminated), length(strlen(data)) {}

    bool defined() const { return data && length>0; }
    const char *get_data() const { return data; }
    int get_length() const { return length; }

    const char *get_following() const { return data ? data+length : NULp; }

    int compare(const char *str) const {
        gb_assert(defined());
        int cmp = strncmp(get_data(), str, get_length());
        if (!cmp) {
            if (str[get_length()]) {
                cmp = -1; // right side contains more content
            }
        }
        return cmp;
    }
    int icompare(const char *str) const {
        gb_assert(defined());
        int cmp = strncasecmp(get_data(), str, get_length());
        if (!cmp) {
            if (str[get_length()]) {
                cmp = -1; // right side contains more content
            }
        }
        return cmp;
    }
    char *copy() const { return ARB_strndup(get_data(), get_length()); }

    char head() const { return defined() ? data[0] : 0; }
    char tail() const { return defined() ? data[length-1] : 0; }

    TextRef headTrimmed() const {
        if (defined()) {
            for (int s = 0; s<length; ++s) {
                if (!isspace(data[s])) {
                    return TextRef(data+s, length-s);
                }
            }
        }
        return TextRef();
    }
    TextRef tailTrimmed() const {
        if (defined()) {
            for (int s = length-1; s>=0; --s) {
                if (!isspace(data[s])) {
                    return TextRef(data, s+1);
                }
            }
        }
        return TextRef();
    }

    TextRef trimmed() const {
        return headTrimmed().tailTrimmed();
    }

    inline TextRef partBefore(const TextRef& subref) const;
    inline TextRef partBehind(const TextRef& subref) const;

    bool is_part_of(const TextRef& other) const {
        gb_assert(defined() && other.defined());
        return get_data()>=other.get_data() && get_following()<=other.get_following();
    }

    const char *find(char c) const { return reinterpret_cast<const char*>(memchr(get_data(), c, get_length())); }
};

static TextRef textBetween(const TextRef& t1, const TextRef& t2) {
    const char *behind_d1 = t1.get_following();
    const char *d2        = t2.get_data();

    if (behind_d1 && d2 && behind_d1<d2) {
        return TextRef(behind_d1, d2-behind_d1);
    }
    return TextRef();
}

inline TextRef TextRef::partBefore(const TextRef& subref) const {
    gb_assert(subref.is_part_of(*this));
    return textBetween(TextRef(get_data(), 0), subref);
}
inline TextRef TextRef::partBehind(const TextRef& subref) const {
    gb_assert(subref.is_part_of(*this));
    return TextRef(subref.get_following(), get_following()-subref.get_following());
}

class TaggedContentParser {
    TextRef wholeInput;
    TextRef tag, content; // current position
    TextRef restTags;     // store (rest of) multiple tags (e.g. from "[t1,t2]")
    TextRef nextBrackets; // next "[..]" part (behind current tag)

    void findBrackets(const char *in) {
        nextBrackets = TextRef();
        const char *tag_start = strchr(in, '[');
        if (tag_start) {
            const char *tag_end = strchr(tag_start, ']');
            if (tag_end) {
                if (tag_end == tag_start+1) { // empty tag -> use as content
                    findBrackets(tag_end+1);
                }
                else {
                    const char *unwanted_bracket = reinterpret_cast<const char*>(memchr(tag_start+1, '[', tag_end-tag_start-1));
                    if (unwanted_bracket) { // tagname contains '[' -> step to next bracket
                        findBrackets(unwanted_bracket);
                    }
                    else {
                        TextRef name = TextRef(tag_start+1, tag_end-tag_start-1).trimmed();
                        if (name.defined()) { // not only whitespace inside brackets
                            nextBrackets = TextRef(tag_start, tag_end-tag_start+1);
                        }
                        else {
                            findBrackets(tag_end+1);
                        }
                    }
                }
            }
        }
    }

    void parse_next_multi_tag() {
        gb_assert(restTags.defined());
        TextRef comma(restTags.find(','), 1);
        if (comma.defined()) {
            tag      = restTags.partBefore(comma).tailTrimmed();
            restTags = restTags.partBehind(comma).headTrimmed();
        }
        else {
            tag      = restTags;
            restTags = TextRef();
        }
    }
    void parse_next() {
        if (restTags.defined()) {
            parse_next_multi_tag();
        }
        else if (nextBrackets.defined()) {
            TextRef brackets = nextBrackets;
            findBrackets(brackets.get_following());

            content = (nextBrackets.defined() ? textBetween(brackets, nextBrackets) : wholeInput.partBehind(brackets)).trimmed();

            gb_assert(brackets.head() == '[' && brackets.tail() == ']');

            TextRef tags = TextRef(brackets.get_data()+1, brackets.get_length()-2).trimmed();
            gb_assert(tags.defined());

            restTags = tags;
            parse_next_multi_tag();
        }
        else {
            tag = content = TextRef();
            gb_assert(!has_part());
        }
    }
    void parse_first() {
        gb_assert(!has_part());
        findBrackets(wholeInput.get_data());
        content = (nextBrackets.defined() ? wholeInput.partBefore(nextBrackets) : wholeInput).trimmed();
        if (!content.defined()) parse_next(); // no untagged prefix seen -> directly goto first tag
    }

public:
    TaggedContentParser(const char *input_) : wholeInput(input_) { parse_first(); }

    bool has_tag() const { return tag.defined(); }
    bool has_content() const { return content.defined(); }

    void next() { parse_next(); }
    bool has_part() const { return has_tag() || has_content(); } // false -> parser has finished

    const TextRef& get_tag() const { return tag; }
    const TextRef& get_content() const { return content; }
};


// -------------------------------------------
//      helper function for tagged fields

static void g_bs_add_value_tag_to_hash(GB_HASH *hash, const char *tag, char *value) {
    if (!value[0]) return; // ignore empty values

    {
        char *p;
        p = value; while ((p = strchr(p, '['))) *p =   '{'; // replace all '[' by '{'
        p = value; while ((p = strchr(p, ']'))) *p =   '}'; // replace all ']' by '}'
    }

    GB_HASH *sh = (GB_HASH *)GBS_read_hash(hash, value);
    if (!sh) {
        sh = GBS_create_hash(10, GB_IGNORE_CASE);        // Tags are case independent
        GBS_write_hash(hash, value, (long)sh);
    }
    GBS_write_hash(sh, tag, 1);
}

static void g_bs_convert_string_to_tagged_hash_with_delete(GB_HASH *hash, char *s, char *default_tag, const char *del) {
    TaggedContentParser parser(s);
    while (parser.has_part()) {
        if (parser.has_content()) {
            char *content = parser.get_content().copy();
            if (parser.has_tag()) {
                char *tag = parser.get_tag().copy();
                if (!del || ARB_stricmp(tag, del) != 0) {
                    g_bs_add_value_tag_to_hash(hash, tag, content);
                }
                free(tag);
            }
            else {
                g_bs_add_value_tag_to_hash(hash, default_tag, content); // no tag found, use default tag
            }
            free(content);
        }
        parser.next();
    }
}

static GB_ERROR g_bs_convert_string_to_tagged_hash_with_rewrite(GB_HASH *hash, char *s, char *default_tag, const char *rtag, const char *aci, GBL_call_env& env) {
    GB_ERROR error = NULp;

    TaggedContentParser parser(s);
    while (parser.has_part() && !error) {
        if (parser.has_content()) {
            char *value = parser.get_content().copy();
            char *tag   = parser.has_tag() ? parser.get_tag().copy() : strdup(default_tag);

            if (rtag && ARB_stricmp(tag, rtag) == 0) {
                freeset(value, GB_command_interpreter_in_env(value, aci, env));
                if (!value) error = GB_await_error();
            }

            if (!error) g_bs_add_value_tag_to_hash(hash, tag, value);

            free(tag);
            free(value);
        }
        parser.next();
    }

    return error;
}

static void g_bs_merge_tags(const char *tag, long /*val*/, void *cd_sub_result) {
    GBS_strstruct& sub_result = *(GBS_strstruct*)cd_sub_result;

    sub_result.cat(tag);
    sub_result.put(',');
}

static void g_bs_read_tagged_hash(const char *value, long subhash, void *cd_g_bs_collect_tags_hash) {
    static int counter = 0;

    GBS_strstruct sub_result(100);
    GBS_hash_do_const_sorted_loop((GB_HASH *)subhash, g_bs_merge_tags, GBS_HCF_sortedByKey, &sub_result);
    sub_result.putlong(counter++); // create a unique number

    char *str = ARB_strupper(sub_result.release());

    GB_HASH *g_bs_collect_tags_hash = (GB_HASH*)cd_g_bs_collect_tags_hash;
    GBS_write_hash(g_bs_collect_tags_hash, str, (long)ARB_strdup(value)); // send output to new hash for sorting

    free(str);
}

static void g_bs_read_final_hash(const char *tag, long value, void *cd_merge_result) {
    GBS_strstruct& merge_result = *(GBS_strstruct*)cd_merge_result;

    char *lk = const_cast<char*>(strrchr(tag, ','));
    if (lk) {           // remove number at end
        *lk = 0;

        if (!merge_result.empty()) merge_result.put(' '); // skip trailing space
        merge_result.cat_wrapped("[]", tag);
        merge_result.put(' ');
    }
    merge_result.cat((char*)value);
}

static char *g_bs_get_string_of_tag_hash(GB_HASH *tag_hash) {
    GBS_strstruct  merge_result(256);
    GB_HASH       *collect_tags_hash = GBS_create_dynaval_hash(512, GB_IGNORE_CASE, GBS_dynaval_free);

    GBS_hash_do_const_sorted_loop(tag_hash,          g_bs_read_tagged_hash, GBS_HCF_sortedByKey, collect_tags_hash); // move everything into collect_tags_hash
    GBS_hash_do_const_sorted_loop(collect_tags_hash, g_bs_read_final_hash,  GBS_HCF_sortedByKey, &merge_result);

    GBS_free_hash(collect_tags_hash);
    return merge_result.release_memfriendly();
}

static long g_bs_free_hash_of_hashes_elem(const char */*key*/, long val, void *) {
    GB_HASH *hash = (GB_HASH*)val;
    if (hash) GBS_free_hash(hash);
    return 0;
}
static void g_bs_free_hash_of_hashes(GB_HASH *hash) {
    GBS_hash_do_loop(hash, g_bs_free_hash_of_hashes_elem, NULp);
    GBS_free_hash(hash);
}

char *GBS_merge_tagged_strings(const char *s1, const char *tag1, const char *replace1, const char *s2, const char *tag2, const char *replace2) {
    /* Create a tagged string from two tagged strings:
     * a tagged string is something like '[tag,tag,tag] string [tag] string [tag,tag] string'
     *
     * if 's2' is not empty, then delete tag 'replace1' in 's1'
     * if 's1' is not empty, then delete tag 'replace2' in 's2'
     *
     * (result should never be NULp)
     */

    char     *str1   = ARB_strdup(s1);
    char     *str2   = ARB_strdup(s2);
    char     *t1     = GBS_string_2_key(tag1);
    char     *t2     = GBS_string_2_key(tag2);
    GB_HASH  *hash   = GBS_create_hash(16, GB_MIND_CASE);

    if (!s1[0]) replace2 = NULp;
    if (!s2[0]) replace1 = NULp;

    if (replace1 && !replace1[0]) replace1 = NULp;
    if (replace2 && !replace2[0]) replace2 = NULp;

    g_bs_convert_string_to_tagged_hash_with_delete(hash, str1, t1, replace1);
    g_bs_convert_string_to_tagged_hash_with_delete(hash, str2, t2, replace2);

    char *result = g_bs_get_string_of_tag_hash(hash);

    g_bs_free_hash_of_hashes(hash);

    free(t2);
    free(t1);
    free(str2);
    free(str1);

    return result;
}

char *GBS_modify_tagged_string_with_ACI(const char *s, const char *dt, const char *tag, const char *aci, GBL_call_env& env) {
    /* if 's' is untagged, tag it with default tag 'dt'.
     * if 'tag' is specified -> apply 'aci' to that part of the content of 's', which is tagged with 'tag' (i.e. look for '[tag]')
     *
     * if result is NULp, an error has been exported.
     */

    char    *str         = ARB_strdup(s);
    char    *default_tag = GBS_string_2_key(dt);
    GB_HASH *hash        = GBS_create_hash(16, GB_MIND_CASE);
    char    *result      = NULp;

    GB_ERROR error = g_bs_convert_string_to_tagged_hash_with_rewrite(hash, str, default_tag, tag, aci, env);

    if (!error) {
        result = g_bs_get_string_of_tag_hash(hash);
    }
    else {
        GB_export_error(error);
    }

    g_bs_free_hash_of_hashes(hash);

    free(default_tag);
    free(str);

    return result;
}

char *GB_read_as_tagged_string(GBDATA *gbd, const char *tagi) {
    char *buf = GB_read_as_string(gbd);
    if (buf && tagi && tagi[0]) {
        TaggedContentParser parser(buf);

        char *wantedTag    = GBS_string_2_key(tagi);
        char *contentFound = NULp;

        while (parser.has_part() && !contentFound) {
            if (parser.has_tag() && parser.get_tag().icompare(wantedTag) == 0) {
                contentFound = parser.get_content().copy();
            }
            parser.next();
        }
        free(wantedTag);
        free(buf);

        return contentFound;
    }
    return buf;
}


/* be CAREFUL : this function is used to save ARB ASCII database (i.e. properties)
 * used as well to save perl macros
 *
 * when changing GBS_fwrite_string -> GBS_fread_string needs to be fixed as well
 *
 * always keep in mind, that many users have databases/macros written with older
 * versions of this function. They MUST load proper!!!
 */
void GBS_fwrite_string(const char *strngi, FILE *out) {
    unsigned char *strng = (unsigned char *)strngi;
    int            c;

    putc('"', out);

    while ((c = *strng++)) {
        if (c < 32) {
            putc('\\', out);
            if (c == '\n')
                putc('n', out);
            else if (c == '\t')
                putc('t', out);
            else if (c<25) {
                putc(c+'@', out); // characters ASCII 0..24 encoded as \@..\X    (\n and \t are done above)
            }
            else {
                putc(c+('0'-25), out); // characters ASCII 25..31 encoded as \0..\6
            }
        }
        else if (c == '"') {
            putc('\\', out);
            putc('"', out);
        }
        else if (c == '\\') {
            putc('\\', out);
            putc('\\', out);
        }
        else {
            putc(c, out);
        }
    }
    putc('"', out);
}

/*  Read a string from a file written by GBS_fwrite_string,
 *  Searches first '"'
 *
 *  WARNING : changing this function affects perl-macro execution (read warnings for GBS_fwrite_string)
 *  any changes should be done in GBS_fconvert_string too.
 */

static char *GBS_fread_string(FILE *in) { // @@@ should be used when reading things written by GBS_fwrite_string, but it's unused!
    GBS_strstruct buf(1024);

    int x;
    while ((x = getc(in)) != '"') if (x == EOF) break;  // Search first '"'

    if (x != EOF) {
        while ((x = getc(in)) != '"') {
            if (x == EOF) break;
            if (x == '\\') {
                x = getc(in);
                if (x==EOF) break;
                if (x == 'n') { buf.put('\n'); continue; }
                if (x == 't') { buf.put('\t'); continue; }
                if (x>='@' && x <= '@' + 25) { buf.put(x-'@'); continue; }
                if (x>='0' && x <= '9')      { buf.put(x-('0'-25)); continue; }
                // all other backslashes are simply skipped
            }
            buf.put(x);
        }
    }
    return buf.release_memfriendly();
}

/* does similar decoding as GBS_fread_string but works directly on an existing buffer
 * (WARNING : GBS_fconvert_string is used by gb_read_file which reads ARB ASCII databases!!)
 *
 * inserts \0 behind decoded string (removes the closing '"')
 * returns a pointer behind the end (") of the _encoded_ string
 * returns NULp if a 0-character is found
 */
char *GBS_fconvert_string(char *buffer) {
    char *t = buffer;
    char *f = buffer;
    int   x;

    gb_assert(f[-1] == '"');
    // the opening " has already been read

    while ((x = *f++) != '"') {
        if (!x) break;

        if (x == '\\') {
            x = *f++;
            if (!x) break;

            if (x == 'n') {
                *t++ = '\n';
                continue;
            }
            if (x == 't') {
                *t++ = '\t';
                continue;
            }
            if (x>='@' && x <= '@' + 25) {
                *t++ = x-'@';
                continue;
            }
            if (x>='0' && x <= '9') {
                *t++ = x-('0'-25);
                continue;
            }
            // all other backslashes are simply skipped
        }
        *t++ = x;
    }

    if (!x) return NULp; // error (string should not contain 0-character)
    gb_assert(x == '"');

    t[0] = 0;
    return f;
}

char *GBS_replace_tabs_by_spaces(const char *text) {
    int           tlen   = strlen(text);
    GBS_strstruct mfile(tlen * 3/2 + 1);
    int           tabpos = 0;
    int           c;

    while ((c=*(text++))) {
        if (c == '\t') {
            int ntab = (tabpos + 8) & 0xfffff8;
            while (tabpos < ntab) {
                mfile.put(' ');
                tabpos++;
            }
            continue;
        }
        tabpos ++;
        if (c == '\n') {
            tabpos = 0;
        }
        mfile.put(c);
    }
    return mfile.release_memfriendly();
}

char *GBS_trim(const char *str) {
    // trim whitespace at beginning and end of 'str'
    const char *whitespace = " \t\n";
    while (str[0] && strchr(whitespace, str[0])) str++;

    const char *end = strchr(str, 0)-1;
    while (end >= str && strchr(whitespace, end[0])) end--;

    return ARB_strpartdup(str, end);
}

static char *dated_info(const char *info) {
    char   *dated_info = NULp;
    time_t  date;

    if (time(&date) != -1) {
        char *dstr = ctime(&date);
        char *nl   = strchr(dstr, '\n');

        if (nl) nl[0] = 0; // cut off LF

        dated_info = GBS_global_string_copy("%s: %s", dstr, info);
    }
    else {
        dated_info = ARB_strdup(info);
    }
    return dated_info;
}

char *GBS_log_action_to(const char *comment, const char *action, bool stamp) {
    /*! concatenates 'comment' and 'action'.
     * '\n' is appended to existing 'comment' and/or 'action' (if missing).
     * @param comment   may be NULp (=> result is 'action')
     * @param action    may NOT be NULp
     * @param stamp     true -> prefix current timestamp in front of 'action'
     * @return heap copy of concatenation
     */
    size_t clen = comment ? strlen(comment) : 0;
    size_t alen = strlen(action);

    GBS_strstruct new_comment(clen+1+(stamp ? 100 : 0)+alen+1+1); // + 2*\n + \0 + space for stamp

    if (comment) {
        new_comment.cat(comment);
        if (clen == 0 || comment[clen-1] != '\n') new_comment.put('\n');
    }

    if (stamp) {
        char *dated_action = dated_info(action);
        new_comment.cat(dated_action);
        free(dated_action);
    }
    else {
        new_comment.cat(action);
    }
    if (alen == 0 || action[alen-1] != '\n') new_comment.put('\n');

    return new_comment.release_memfriendly();
}

const char *GBS_funptr2readable(void *funptr, bool stripARBHOME) {
    // only returns module and offset for static functions :-(
    char       **funNames     = backtrace_symbols(&funptr, 1);
    const char  *readable_fun = funNames[0];

    if (stripARBHOME) {
        const char *ARBHOME = GB_getenvARBHOME();
        if (ARB_strBeginsWith(readable_fun, ARBHOME)) {
            readable_fun += strlen(ARBHOME)+1; // +1 hides slash behind ARBHOME
        }
    }
    return readable_fun;
}

// --------------------------------------------------------------------------------

#ifdef UNIT_TESTS

#include <test_unit.h>

// #define TEST_TEST_MACROS

#ifdef ENABLE_CRASH_TESTS
static void provokesegv() { raise(SIGSEGV); }
static void dont_provokesegv() {}
# if defined(ASSERTION_USED)
static void failassertion() { gb_assert(0); }
#  if defined(TEST_TEST_MACROS)
static void dont_failassertion() {}
#  endif
static void provokesegv_does_not_fail_assertion() {
    // provokesegv does not raise assertion
    // -> the following assertion fails
    TEST_EXPECT_CODE_ASSERTION_FAILS(provokesegv);
}
# endif
#endif

void TEST_signal_tests__crashtest() {
    // check whether we can test that no SEGV or assertion failure happened
    TEST_EXPECT_NO_SEGFAULT(dont_provokesegv);

    // check whether we can test for SEGV and assertion failures
    TEST_EXPECT_SEGFAULT(provokesegv);
    TEST_EXPECT_CODE_ASSERTION_FAILS(failassertion);

    // tests whether signal suppression works multiple times (by repeating tests)
    TEST_EXPECT_CODE_ASSERTION_FAILS(failassertion);
    TEST_EXPECT_SEGFAULT(provokesegv);

    // test whether SEGV can be distinguished from assertion
    TEST_EXPECT_CODE_ASSERTION_FAILS(provokesegv_does_not_fail_assertion);

    // The following section is disabled, because it will
    // provoke test warnings (to test these warnings).
    // (enable it when changing any of these TEST_..-macros used here)
#if defined(TEST_TEST_MACROS)
    TEST_EXPECT_NO_SEGFAULT__WANTED(provokesegv);

    TEST_EXPECT_SEGFAULT__WANTED(dont_provokesegv);
    TEST_EXPECT_SEGFAULT__UNWANTED(provokesegv);
#if defined(ASSERTION_USED)
    TEST_EXPECT_SEGFAULT__UNWANTED(failassertion);
#endif

    TEST_EXPECT_CODE_ASSERTION_FAILS__WANTED(dont_failassertion);
    TEST_EXPECT_CODE_ASSERTION_FAILS__UNWANTED(failassertion);
    TEST_EXPECT_CODE_ASSERTION_FAILS__UNWANTED(provokesegv_does_not_fail_assertion);
#endif
}

#define TEST_SHORTENED_EQUALS(Long,Short) do {  \
        char *buf = ARB_strdup(Long);           \
        GBS_shorten_repeated_data(buf);         \
        TEST_EXPECT_EQUAL(buf, Short);          \
        free(buf);                              \
    } while(0)

void TEST_GBS_shorten_repeated_data() {
    TEST_SHORTENED_EQUALS("12345", "12345");
    TEST_SHORTENED_EQUALS("aaaaaaaaaaaabc", "a{12}bc");
    TEST_SHORTENED_EQUALS("aaaaaaaaaaabc", "a{11}bc");
    TEST_SHORTENED_EQUALS("aaaaaaaaaabc", "a{10}bc");
    TEST_SHORTENED_EQUALS("aaaaaaaaabc", "a{9}bc");
    TEST_SHORTENED_EQUALS("aaaaaaaabc", "a{8}bc");
    TEST_SHORTENED_EQUALS("aaaaaaabc", "a{7}bc");
    TEST_SHORTENED_EQUALS("aaaaaabc", "a{6}bc");
    TEST_SHORTENED_EQUALS("aaaaabc", "a{5}bc");
    TEST_SHORTENED_EQUALS("aaaabc", "aaaabc");
    TEST_SHORTENED_EQUALS("aaabc", "aaabc");
    TEST_SHORTENED_EQUALS("aabc", "aabc");
    TEST_SHORTENED_EQUALS("", "");
}

static const char *hkey_format[] = {
    "/%s/bbb/ccc",
    "/aaa/%s/ccc",
    "/aaa/bbb/%s",
};

inline const char *useInHkey(const char *fragment, size_t pos) {
    return GBS_global_string(hkey_format[pos], fragment);
}

#define TEST_IN_HKEYS_USING_EXPECT_NO_ERROR(use) do {                   \
        for (size_t i = 0; i<ARRAY_ELEMS(hkey_format); ++i) {           \
            const char *hkey = useInHkey(use, i);                       \
            TEST_ANNOTATE(hkey);                                        \
            TEST_EXPECT_NO_ERROR(GB_check_hkey(hkey));                  \
        }                                                               \
        TEST_ANNOTATE(NULp);                                            \
    } while(0)

#define TEST_IN_HKEYS_USING_EXPECT_ERROR_CONTAINS(use,contains) do {    \
        for (size_t i = 0; i<ARRAY_ELEMS(hkey_format); ++i) {           \
            const char *hkey = useInHkey(use, i);                       \
            TEST_ANNOTATE(hkey);                                        \
            TEST_EXPECT_ERROR_CONTAINS(GB_check_hkey(hkey), contains);  \
        }                                                               \
        TEST_ANNOTATE(NULp);                                            \
    } while(0)


void TEST_DB_key_checks() {
    // plain keys
    const char *shortest  = "ab";
    const char *too_long  = "ab345678901234567890123456789012345678901234567890123456789012345";
    const char *too_short = shortest+1;
    const char *longest   = too_long+1;

    const char *empty  = "";
    const char *slash  = "sub/key";
    const char *dslash = "sub//key";
    const char *comma  = "no,key";
    const char *minus  = "no-key";

    // obsolete GB_LINK syntax:
    const char *link    = "link->syntax";
    const char *nowhere = "link->";
    const char *fromNw  = "->syntax";

    TEST_EXPECT_NO_ERROR(GB_check_key(shortest));
    TEST_EXPECT_NO_ERROR(GB_check_key(longest));

    TEST_EXPECT_ERROR_CONTAINS(GB_check_key(too_short), "too short");
    TEST_EXPECT_ERROR_CONTAINS(GB_check_key(too_long),  "too long");
    TEST_EXPECT_ERROR_CONTAINS(GB_check_key(empty),     "not allowed");

    TEST_EXPECT_ERROR_CONTAINS(GB_check_key(slash),   "Invalid character '/'");
    TEST_EXPECT_ERROR_CONTAINS(GB_check_key(dslash),  "Invalid character '/'");
    TEST_EXPECT_ERROR_CONTAINS(GB_check_key(comma),   "Invalid character ','");
    TEST_EXPECT_ERROR_CONTAINS(GB_check_key(minus),   "Invalid character '-'");
    TEST_EXPECT_ERROR_CONTAINS(GB_check_key(link),    "Invalid character '-'");
    TEST_EXPECT_ERROR_CONTAINS(GB_check_key(nowhere), "Invalid character '-'");
    TEST_EXPECT_ERROR_CONTAINS(GB_check_key(fromNw),  "Invalid character '-'");

    // hierarchical keys
    TEST_IN_HKEYS_USING_EXPECT_NO_ERROR(shortest);
    TEST_IN_HKEYS_USING_EXPECT_NO_ERROR(longest);

    TEST_IN_HKEYS_USING_EXPECT_ERROR_CONTAINS(too_short, "too short");
    TEST_IN_HKEYS_USING_EXPECT_ERROR_CONTAINS(too_long,  "too long");
    TEST_IN_HKEYS_USING_EXPECT_ERROR_CONTAINS(empty,     "not allowed");

    TEST_IN_HKEYS_USING_EXPECT_NO_ERROR(slash);
    TEST_IN_HKEYS_USING_EXPECT_ERROR_CONTAINS(dslash,  "Empty key is not allowed");
    TEST_IN_HKEYS_USING_EXPECT_ERROR_CONTAINS(comma,   "Invalid character ','");
    TEST_IN_HKEYS_USING_EXPECT_ERROR_CONTAINS(minus,   "Invalid character '-'");
    TEST_IN_HKEYS_USING_EXPECT_ERROR_CONTAINS(link,    "Invalid character '-'");
    TEST_IN_HKEYS_USING_EXPECT_ERROR_CONTAINS(nowhere, "Invalid character '-'");
    TEST_IN_HKEYS_USING_EXPECT_ERROR_CONTAINS(fromNw,  "Invalid character '-'");

    // test NULp keys:
    TEST_EXPECT_ERROR_CONTAINS(GB_check_key (NULp), "Empty key is not allowed");
    TEST_EXPECT_ERROR_CONTAINS(GB_check_hkey(NULp), "Empty key is not allowed");

    // some edge cases for hierarchical keys:
    TEST_EXPECT_ERROR_CONTAINS(GB_check_hkey("//"),    "Empty key is not allowed");
    TEST_EXPECT_ERROR_CONTAINS(GB_check_hkey("//key"), "Empty key is not allowed"); // @@@ is double slash compensated by GB_search etc? if yes -> accept here as well!
    TEST_EXPECT_ERROR_CONTAINS(GB_check_hkey("key//"), "Empty key is not allowed");
    TEST_EXPECT_ERROR_CONTAINS(GB_check_hkey("/"),     "Empty key is not allowed");
    TEST_EXPECT_NO_ERROR      (GB_check_hkey("/key"));
    TEST_EXPECT_ERROR_CONTAINS(GB_check_hkey("key/"),  "Empty key is not allowed"); // @@@ use better message? e.g. "invalid trailing '/'"
    TEST_EXPECT_ERROR_CONTAINS(GB_check_hkey(""),      "Empty key is not allowed");
}

#define TEST_STRING2KEY(str,expected) do {              \
        char *as_key = GBS_string_2_key(str);           \
        TEST_EXPECT_EQUAL(as_key, expected);            \
        TEST_EXPECT_NO_ERROR(GB_check_key(as_key));     \
        free(as_key);                                   \
    } while(0)

void TEST_DB_key_generation() {
    TEST_STRING2KEY("abc", "abc");
    TEST_STRING2KEY("a b c", "a_b_c");

    // invalid chars
    TEST_STRING2KEY("string containing \"double-quotes\", 'quotes' and other:shit!*&^@!%@(",
                    "string_containing_doublequotes_quotes_and_othershit");

    // length tests
    TEST_STRING2KEY("a", "a_");                                                          // too short
    TEST_STRING2KEY("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", // too long
                    "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
}

void TEST_TaggedContentParser() {
    // test helper class TextRef:
    TEST_REJECT(TextRef().defined()); // default to undefined
    {
        TextRef bla("blakjahd", 3);
        TEST_EXPECT(bla.defined());
        TEST_EXPECT_EQUAL(bla.get_length(), 3);

        TEST_EXPECT(bla.compare("bl")     > 0);
        TEST_EXPECT(bla.compare("bla") == 0);
        TEST_EXPECT(bla.compare("blase")  < 0);

        TextRef spaced("   spaced   "+1, 10);
        TEST_EXPECT(spaced.headTrimmed().compare("spaced  ") == 0);
        TEST_EXPECT(spaced.tailTrimmed().compare("  spaced") == 0);
        TEST_EXPECT(spaced.trimmed    ().compare("spaced")   == 0);
    }

    const char *text = "  untagged    [tag]  tagged   [empty]   ";

    TextRef cr_untagged(strstr(text, "untagged"), 8);
    TextRef cr_tagged  (strstr(text, "tagged"),   6);
    TextRef tr_tag     (strstr(text, "tag"),      3);
    TextRef tr_empty   (strstr(text, "empty"),    5);

    // test TaggedContentParser:
    {
        TaggedContentParser parser(text);

        TEST_EXPECT(parser.has_part());
        TEST_REJECT(parser.has_tag());
        TEST_EXPECT(parser.get_content().compare("untagged") == 0);

        parser.next();

        TEST_EXPECT(parser.has_part());
        TEST_EXPECT(parser.get_tag    ().compare("tag")    == 0);
        TEST_EXPECT(parser.get_content().compare("tagged") == 0);

        parser.next();

        TEST_EXPECT(parser.has_part());
        TEST_EXPECT(parser.get_tag().compare("empty") == 0);
        TEST_REJECT(parser.has_content());

        parser.next();

        TEST_REJECT(parser.has_part());
    }
    { // parse untagged input
        TaggedContentParser parser("hi");
        TEST_EXPECT(parser.has_part());
        TEST_REJECT(parser.has_tag());
        TEST_EXPECT(parser.get_content().compare("hi") == 0);
        parser.next();
        TEST_REJECT(parser.has_part());
    }
    { // parse empty input
        TaggedContentParser empty("");        TEST_REJECT(empty.has_part());
        TaggedContentParser white("  \t\n "); TEST_REJECT(white.has_part());
    }
    { // parse single tag w/o content
        TaggedContentParser parser(" [hello] ");
        TEST_EXPECT(parser.has_part());
        TEST_EXPECT(parser.get_tag().compare("hello") == 0);
        TEST_REJECT(parser.has_content());
        parser.next();
        TEST_REJECT(parser.has_part());
    }
    { // parse multi-tags
        TaggedContentParser parser(" [ t1 , t2 ] t");
        TEST_EXPECT(parser.has_part());
        TEST_EXPECT(parser.get_tag().compare("t1") == 0);
        TEST_EXPECT(parser.get_content().compare("t") == 0);
        parser.next();
        TEST_EXPECT(parser.has_part());
        TEST_EXPECT(parser.get_tag().compare("t2") == 0);
        TEST_EXPECT(parser.get_content().compare("t") == 0);
        parser.next();
        TEST_REJECT(parser.has_part());
    }
}

#define TEST_MERGE_TAGGED(t1,t2,r1,r2,s1,s2,expected) do {               \
        char *result = GBS_merge_tagged_strings(s1, t1, r1, s2, t2, r2); \
        TEST_EXPECT_EQUAL(result, expected);                             \
        free(result);                                                    \
    } while(0)

#define TEST_MERGE_TAGGED__BROKEN(t1,t2,r1,r2,s1,s2,expected,got) do {   \
        char *result = GBS_merge_tagged_strings(s1, t1, r1, s2, t2, r2); \
        TEST_EXPECT_EQUAL__BROKEN(result, expected, got);                \
        free(result);                                                    \
    } while(0)

void TEST_merge_tagged_strings() {
    // merge two fields:
    const char *_0 = NULp;

    TEST_MERGE_TAGGED("S",   "D",   "", "", "source", "dest", "[D_] dest [S_] source");
    TEST_MERGE_TAGGED("SRC", "DST", "", _0, "source", "dest", "[DST] dest [SRC] source");
    TEST_MERGE_TAGGED("SRC", "DST", _0, "", "source", "dest", "[DST] dest [SRC] source");
    TEST_MERGE_TAGGED("SRC", "DST", _0, _0, "sth",    "sth",  "[DST,SRC] sth");

    TEST_MERGE_TAGGED("SRC", "DST", "SRC", "DST", "sth",           "sth",           "[DST,SRC] sth"); // show default tags do not get deleted
    TEST_MERGE_TAGGED("SRC", "DST", "SRC", "DST", "sth [SRC] del", "sth [DST] del", "[DST,SRC] sth"); // exception: already present default tags

    // update fields:
    TEST_MERGE_TAGGED("SRC", "DST", _0, "SRC", "newsource", " [DST] dest [SRC] source", "[DST] dest [SRC] newsource");
    TEST_MERGE_TAGGED("SRC", "DST", _0, "SRC", "newsource", " [DST,SRC] sth",           "[DST] sth [SRC] newsource");
    TEST_MERGE_TAGGED("SRC", "DST", _0, "SRC", "newsource", " [DST,src] sth",           "[DST] sth [SRC] newsource");
    TEST_MERGE_TAGGED("SRC", "DST", _0, "src", "newsource", " [DST,SRC] sth",           "[DST] sth [SRC] newsource");
    TEST_MERGE_TAGGED("SRC", "DST", _0, "SRC", "sth",       " [DST] sth [SRC] source",  "[DST,SRC] sth");

    // append (opposed to update this keeps old entries with same tag; useless?)
    TEST_MERGE_TAGGED("SRC", "DST", _0, _0, "newsource", "[DST] dest [SRC] source", "[DST] dest [SRC] newsource [SRC] source");
    TEST_MERGE_TAGGED("SRC", "DST", _0, _0, "newsource", "[DST,SRC] sth",           "[DST,SRC] sth [SRC] newsource");
    TEST_MERGE_TAGGED("SRC", "DST", _0, _0, "sth",       "[DST] sth [SRC] source",  "[DST,SRC] sth [SRC] source");

    // merge three fields:
    TEST_MERGE_TAGGED("OTH", "DST", _0, _0, "oth",    " [DST] dest [SRC] source", "[DST] dest [OTH] oth [SRC] source");
    TEST_MERGE_TAGGED("OTH", "DST", _0, _0, "oth",    " [DST,SRC] sth",           "[DST,SRC] sth [OTH] oth");
    TEST_MERGE_TAGGED("OTH", "DST", _0, _0, "sth",    " [DST,SRC] sth",           "[DST,OTH,SRC] sth");
    TEST_MERGE_TAGGED("OTH", "DST", _0, _0, "dest",   " [DST] dest [SRC] source", "[DST,OTH] dest [SRC] source");
    TEST_MERGE_TAGGED("OTH", "DST", _0, _0, "source", " [DST] dest [SRC] source", "[DST] dest [OTH,SRC] source");

    // same tests as in section above, but vv:
    TEST_MERGE_TAGGED("DST", "OTH", _0, _0, " [DST] dest [SRC] source", "oth",    "[DST] dest [OTH] oth [SRC] source");
    TEST_MERGE_TAGGED("DST", "OTH", _0, _0, " [DST,SRC] sth",           "oth",    "[DST,SRC] sth [OTH] oth");
    TEST_MERGE_TAGGED("DST", "OTH", _0, _0, " [DST,SRC] sth",           "sth",    "[DST,OTH,SRC] sth");
    TEST_MERGE_TAGGED("DST", "OTH", _0, _0, " [DST] dest [SRC] source", "dest",   "[DST,OTH] dest [SRC] source");
    TEST_MERGE_TAGGED("DST", "OTH", _0, _0, " [DST] dest [SRC] source", "source", "[DST] dest [OTH,SRC] source");

    // test real-merges (content existing in both strings):
    TEST_MERGE_TAGGED("P1", "P2", _0, _0, " pre1 [C1] c1 [C2] c2",              "pre2[C2]c2[C3]c3",            "[C1] c1 [C2] c2 [C3] c3 [P1] pre1 [P2] pre2");
    TEST_MERGE_TAGGED("P1", "P2", _0, _0, " pre [C1] c1 [C2] c2",               "pre [C2]c2 [C3]c3",           "[C1] c1 [C2] c2 [C3] c3 [P1,P2] pre");                 // identical content for [C2]
    TEST_MERGE_TAGGED("P1", "P2", _0, _0, " pre [C1] c1 [C2] c2",               "pre [c2]c2 [C3]c3",           "[C1] c1 [C2] c2 [C3] c3 [P1,P2] pre");                 // identical content + different tag-case for [C2] (tests that tags are case-insensitive!)
    TEST_MERGE_TAGGED("P1", "P2", _0, _0, " pre [C1] c1 [C2] c2a",              "pre [C2]c2b [C3]c3",          "[C1] c1 [C2] c2a [C2] c2b [C3] c3 [P1,P2] pre");       // different content for [C2] -> inserts that tag multiple times
    TEST_MERGE_TAGGED("P1", "P2", _0, _0, " [C1] c1 [C2] c2a [C2] c2b [C3] c3", "[C3]c3",                      "[C1] c1 [C2] c2a [C2] c2b [C3] c3");                   // continue processing last result (multiple tags with same name are handled)
    TEST_MERGE_TAGGED("P1", "P2", _0, _0, " [C1] c1 [C2] c2a [C2] c2b [C3] c3", "[C2] c2b [C3]c3 [C2] c2a",    "[C1] c1 [C2] c2a [C2] c2b [C3] c3");                   // merge multiple tags with same name
    TEST_MERGE_TAGGED("P1", "P2", _0, _0, " pre [C1] c1 [C2] c2a",              "pre [c2]c2b [C3]c3",          "[C1] c1 [C2] c2a [C2] c2b [C3] c3 [P1,P2] pre");       // different content and different tag-case for [C2]
    TEST_MERGE_TAGGED("P1", "P2", _0, _0, " pre [C1,C4]  c1 [C2] c2a   ",       "pre [c2]  c2b [C4,C3]c3",     "[C1,C4] c1 [C2] c2a [C2] c2b [C3,C4] c3 [P1,P2] pre"); // multitags
    TEST_MERGE_TAGGED("P1", "P2", _0, _0, " pre [ C1, C4]  c1 [C2 ] c2a   ",    "pre [ c2]  c2b [C4, C3  ]c3", "[C1,C4] c1 [C2] c2a [C2] c2b [C3,C4] c3 [P1,P2] pre"); // spaced-multitags

    // merge two tagged string with deleting
#define DSTSRC1    "[DST] dest1 [SRC] src1"
#define DSTSRC2    "[DST] dest2 [SRC] src2"
#define DSTSRC2LOW "[dst] dest2 [src] src2"

    TEST_MERGE_TAGGED("O1", "O2", _0,        _0,        DSTSRC1, DSTSRC2,    "[DST] dest1 [DST] dest2 [SRC] src1 [SRC] src2");
    TEST_MERGE_TAGGED("O1", "O2", "SRC",     _0,        DSTSRC1, DSTSRC2,    "[DST] dest1 [DST] dest2 [SRC] src2");
    TEST_MERGE_TAGGED("O1", "O2", _0,        "DST",     DSTSRC1, DSTSRC2,    "[DST] dest1 [SRC] src1 [SRC] src2");
    TEST_MERGE_TAGGED("O1", "O2", "SRC",     "DST",     DSTSRC1, DSTSRC2,    "[DST] dest1 [SRC] src2");
    TEST_MERGE_TAGGED("O1", "O2", "SRC",     "DST",     DSTSRC1, DSTSRC2LOW, "[DST] dest1 [SRC] src2");
    TEST_MERGE_TAGGED("O1", "O2", "src",     "DST",     DSTSRC1, DSTSRC2,    "[DST] dest1 [SRC] src2");
    TEST_MERGE_TAGGED("O1", "O2", "src",     "DST",     DSTSRC1, DSTSRC2LOW, "[DST] dest1 [SRC] src2");
    TEST_MERGE_TAGGED("O1", "O2", "SRC",     "dst",     DSTSRC1, DSTSRC2,    "[DST] dest1 [SRC] src2");
    TEST_MERGE_TAGGED("O1", "O2", "SRC",     "dst",     DSTSRC1, DSTSRC2LOW, "[DST] dest1 [SRC] src2");
    TEST_MERGE_TAGGED("O1", "O2", "DST",     "SRC",     DSTSRC1, DSTSRC2,    "[DST] dest2 [SRC] src1");
    TEST_MERGE_TAGGED("O1", "O2", "DST",     "SRC",     DSTSRC1, DSTSRC2LOW, "[DST] dest2 [SRC] src1");
    TEST_MERGE_TAGGED("O1", "O2", "dst",     "src",     DSTSRC1, DSTSRC2,    "[DST] dest2 [SRC] src1");
    TEST_MERGE_TAGGED("O1", "O2", "dst",     "src",     DSTSRC1, DSTSRC2LOW, "[DST] dest2 [SRC] src1");
    TEST_MERGE_TAGGED("O1", "O2", "SRC,DST", "DST,SRC", DSTSRC1, DSTSRC2,    "[DST] dest1 [DST] dest2 [SRC] src1 [SRC] src2"); // delete does not handle multiple tags (yet)
}

__ATTR__REDUCED_OPTIMIZE void TEST_read_tagged() {
    GB_shell  shell;
    GBDATA   *gb_main = GB_open("new.arb", "c");
    {
        GB_transaction ta(gb_main);

        {
            GBDATA *gb_int_entry = GB_create(gb_main, "int", GB_INT);
            TEST_EXPECT_NO_ERROR(GB_write_int(gb_int_entry, 4711));
            TEST_EXPECT_NORESULT__NOERROREXPORTED(GB_read_as_tagged_string(gb_int_entry, "USELESS")); // reading from GB_INT doesn't make sense, but has to work w/o error

            GBDATA   *gb_ints_entry = GB_create(gb_main, "int", GB_INTS);
            GB_UINT4  ints[] = { 1, 2 };
            TEST_EXPECT_NO_ERROR(GB_write_ints(gb_ints_entry, ints, 2));
            TEST_EXPECT_NORESULT__NOERROREXPORTED(GB_read_as_tagged_string(gb_ints_entry, "USELESS")); // reading from GB_INTS doesn't make sense, but has to work w/o error
        }

#define TEST_EXPECT_TAG_CONTENT(tag,expected) TEST_EXPECT_EQUAL_STRINGCOPY__NOERROREXPORTED(GB_read_as_tagged_string(gb_entry, tag), expected)
#define TEST_REJECT_TAG_CONTENT(tag)          TEST_EXPECT_NORESULT__NOERROREXPORTED(GB_read_as_tagged_string(gb_entry, tag))
#define TEST_EXPECT_FULL_CONTENT(tag)         TEST_EXPECT_TAG_CONTENT(tag,tagged_string)

        GBDATA     *gb_entry      = GB_create(gb_main, "str", GB_STRING);
        const char *tagged_string = "[T1,T2] t12 [T3] t3[T4]t4[][]xxx[AA]aa[WW]w1 [WW]w2 [BB]bb [XX]x1 [XX]x2 [yy]  yy  [Y] y [EMPTY][FAKE,EMPTY]fake[ SP1ST, SPACED, PADDED ,UNSPACED,_SCORED_,FOLLOWED ,FOLLAST ]    spaced   [LAST]  last  ";
        TEST_EXPECT_NO_ERROR(GB_write_string(gb_entry, tagged_string));

        TEST_EXPECT_FULL_CONTENT(NULp);
        TEST_EXPECT_FULL_CONTENT("");
        TEST_REJECT_TAG_CONTENT(" "); // searches for tag '_' (no such tag)

        TEST_EXPECT_TAG_CONTENT("T1", "t12");
        TEST_EXPECT_TAG_CONTENT("T2", "t12");
        TEST_EXPECT_TAG_CONTENT("T3", "t3");
        TEST_EXPECT_TAG_CONTENT("T4", "t4[][]xxx");

        TEST_EXPECT_TAG_CONTENT("AA", "aa");
        TEST_EXPECT_TAG_CONTENT("BB", "bb");
        TEST_EXPECT_TAG_CONTENT("WW", "w1"); // now finds 1st occurrence of [WW]
        TEST_EXPECT_TAG_CONTENT("XX", "x1");
        TEST_EXPECT_TAG_CONTENT("YY", "yy");
        TEST_EXPECT_TAG_CONTENT("yy", "yy");

        TEST_REJECT_TAG_CONTENT("Y");
        // TEST_EXPECT_TAG_CONTENT("Y",  "y"); // @@@ tags with length == 1 are never found -> should be handled when used via GUI

        TEST_EXPECT_TAG_CONTENT("EMPTY", "fake"); // now reports 1st non-empty content
        TEST_EXPECT_TAG_CONTENT("FAKE",  "fake");
        TEST_EXPECT_TAG_CONTENT("fake",  "fake");

        TEST_REJECT_TAG_CONTENT("NOSUCHTAG");
        TEST_EXPECT_TAG_CONTENT("SPACED", "spaced");
        TEST_EXPECT_TAG_CONTENT("SP1ST",  "spaced");
        TEST_REJECT_TAG_CONTENT(" SPACED");         // dito (specified space is converted into '_' before searching tag)
        TEST_REJECT_TAG_CONTENT("_SPACED");         // not found (tag stored with space, search performed for '_SPACED')
        TEST_EXPECT_TAG_CONTENT("PADDED", "spaced");
        TEST_EXPECT_TAG_CONTENT("FOLLOWED", "spaced");
        TEST_EXPECT_TAG_CONTENT("FOLLAST",  "spaced");

        TEST_EXPECT_TAG_CONTENT("_SCORED_", "spaced");
        TEST_EXPECT_TAG_CONTENT(" SCORED ", "spaced");
        TEST_EXPECT_TAG_CONTENT("UNSPACED", "spaced");
        TEST_EXPECT_TAG_CONTENT("LAST",     "last");

        // test incomplete tags
        tagged_string = "bla [WHATEVER   hello";
        TEST_EXPECT_NO_ERROR(GB_write_string(gb_entry, tagged_string));
        TEST_REJECT_TAG_CONTENT("WHATEVER");

        tagged_string = "bla [T1] t1 [T2 t2 [T3] t3";
        TEST_EXPECT_NO_ERROR(GB_write_string(gb_entry, tagged_string));
        TEST_EXPECT_TAG_CONTENT("T1", "t1 [T2 t2");
        TEST_REJECT_TAG_CONTENT("T2"); // tag is unclosed
        TEST_EXPECT_TAG_CONTENT("T3", "t3");

        // test pathological tags
        tagged_string = "bla [T1] t1 [ ] sp1 [  ] sp2 [___] us [T3] t3 [_a] a";
        TEST_EXPECT_NO_ERROR(GB_write_string(gb_entry, tagged_string));
        TEST_EXPECT_TAG_CONTENT("T1", "t1 [ ] sp1 [  ] sp2");
        TEST_EXPECT_FULL_CONTENT("");
        TEST_REJECT_TAG_CONTENT(" ");
        TEST_REJECT_TAG_CONTENT("  ");
        TEST_REJECT_TAG_CONTENT(",");
        TEST_EXPECT_TAG_CONTENT(", a",  "a");  // searches for tag '_a'
        TEST_EXPECT_TAG_CONTENT(", a,", "a");  // dito
        TEST_EXPECT_TAG_CONTENT(", ,a,", "a"); // dito
        TEST_EXPECT_TAG_CONTENT("   ",  "us");
        TEST_EXPECT_TAG_CONTENT("T3",   "t3");
    }
    GB_close(gb_main);
}

#define TEST_EXPECT_EVAL_TAGGED(in,dtag,tag,aci,expected) do{                   \
        TEST_EXPECT_EQUAL_STRINGCOPY__NOERROREXPORTED(                          \
            GBS_modify_tagged_string_with_ACI(in, dtag, tag, aci, callEnv),     \
            expected);                                                          \
    }while(0)

#define TEST_EXPECT_EVAL_TAGGED_ERROR_EXPORTED(in,dtag,tag,aci,expectedErrorPart) do{   \
        TEST_EXPECT_NORESULT__ERROREXPORTED_CONTAINS(                                   \
            GBS_modify_tagged_string_with_ACI(in, dtag, tag, aci, callEnv),             \
            expectedErrorPart);                                                         \
    }while(0)

__ATTR__REDUCED_OPTIMIZE void TEST_tagged_eval() {
    GB_shell  shell;
    GBDATA   *gb_main = GB_open("TEST_loadsave.arb", "r");
    {
        GB_transaction ta(gb_main);
        GBL_env        env(gb_main, "tree_missing");

        {
            GBDATA *gb_species = GBT_find_species(gb_main, "MhcBurto");
            TEST_REJECT_NULL(gb_species);
            GBL_call_env  callEnv(gb_species, env);

            TEST_EXPECT_EVAL_TAGGED("bla", "def", "tag", "",          "[DEF] bla");
            TEST_EXPECT_EVAL_TAGGED("bla", "def", "tag", NULp,        "[DEF] bla");
            TEST_EXPECT_EVAL_TAGGED("bla", "def", "tag", ":bla=blub", "[DEF] bla");
            TEST_EXPECT_EVAL_TAGGED("bla", "tag", "tag", ":bla=blub", "[TAG] blub");
            TEST_EXPECT_EVAL_TAGGED("bla", "tag", "tag", "len",       "[TAG] 3");

            // empty tags:
            TEST_EXPECT_EVAL_TAGGED("[empty] ",               "def", "empty", NULp, "");
            TEST_EXPECT_EVAL_TAGGED("[empty] [filled] xxx",   "def", "empty", NULp, "[FILLED] xxx");
            TEST_EXPECT_EVAL_TAGGED("[empty]   [filled] xxx", "def", "empty", NULp, "[FILLED] xxx");
            TEST_EXPECT_EVAL_TAGGED("[empty][filled] xxx",    "def", "empty", NULp, "[FILLED] xxx");
            TEST_EXPECT_EVAL_TAGGED("[filled] xxx [empty]",   "def", "empty", NULp, "[FILLED] xxx");

#define THREE_TAGS        "[TAG] tag [tip] tip [top] top"
#define THREE_TAGS_UPCASE "[TAG] tag [TIP] tip [TOP] top"

            // dont eval:
            TEST_EXPECT_EVAL_TAGGED(THREE_TAGS, "def", "TAG", NULp,      THREE_TAGS_UPCASE);
            // eval SRT:
            TEST_EXPECT_EVAL_TAGGED(THREE_TAGS, "def", "TAG", ":*=<*>",  "[TAG] <tag> [TIP] tip [TOP] top");
            TEST_EXPECT_EVAL_TAGGED(THREE_TAGS, "def", "tag", ":*=<*>",  "[TAG] <tag> [TIP] tip [TOP] top");
            TEST_EXPECT_EVAL_TAGGED(THREE_TAGS, "def", "tip", ":*=(*)",  "[TAG] tag [TIP] (tip) [TOP] top");
            TEST_EXPECT_EVAL_TAGGED(THREE_TAGS, "def", "TIP", ":*=(*)",  "[TAG] tag [TIP] (tip) [TOP] top");
            TEST_EXPECT_EVAL_TAGGED(THREE_TAGS, "def", "tip", ":*=",     "[TAG] tag [TOP] top");               // tag emptied by SRT was removed from result
            TEST_EXPECT_EVAL_TAGGED(THREE_TAGS, "def", "top", ":*=*-*1", "[TAG] tag [TIP] tip [TOP] top-top");
            TEST_EXPECT_EVAL_TAGGED(THREE_TAGS, "def", "tip", ":i=o",    "[TAG] tag [TIP,TOP] top");           // merge tags
            // eval ACI:
            TEST_EXPECT_EVAL_TAGGED(THREE_TAGS, "def", "tip", "len", "[TAG] tag [TIP] 3 [TOP] top");
            TEST_EXPECT_EVAL_TAGGED(THREE_TAGS, "def", "top", "len", "[TAG] tag [TIP] tip [TOP] 3");

            // test SRT/ACI errors:
            TEST_EXPECT_EVAL_TAGGED_ERROR_EXPORTED(THREE_TAGS,   "def", "top", ":*",     "no '=' found");
            TEST_EXPECT_EVAL_TAGGED_ERROR_EXPORTED("untagged",   "def", "def", ":*",     "no '=' found");
            TEST_EXPECT_EVAL_TAGGED_ERROR_EXPORTED(THREE_TAGS,   "def", "top", "illcmd", "Unknown command 'illcmd'");
            TEST_EXPECT_EVAL_TAGGED_ERROR_EXPORTED("un [tagged", "def", "def", "illcmd", "Unknown command 'illcmd'");

            // no error raised, if expression not applied:
            TEST_EXPECT_EVAL_TAGGED(THREE_TAGS, "def", "no", "illcmd", THREE_TAGS_UPCASE);

            // incomplete tags
            TEST_EXPECT_EVAL_TAGGED("[no tag",      "def", "def", ":*=<*>",       "[DEF] <{no tag>");
            TEST_EXPECT_EVAL_TAGGED("[no tag",      "def", "def", ":* *=<*2,*1>", "[DEF] <tag,{no>");
            TEST_EXPECT_EVAL_TAGGED("[no [tag",     "def", "def", ":* *=<*2,*1>", "[DEF] <{tag,{no>");
            TEST_EXPECT_EVAL_TAGGED("[no [tag] xx", "def", "def", ":* *=<*2,*1>", "[DEF] {no [TAG] xx");           // SRT changes nothing here (no match)
            TEST_EXPECT_EVAL_TAGGED("[no [tag[]",   "def", "def", ":* *=<*2,*1>", "[DEF] <{tag{},{no>");
            TEST_EXPECT_EVAL_TAGGED("[no [tag[] xx","def", "def", ":* *=<*2,*1>", "[DEF] <{tag{} xx,{no>");
            TEST_EXPECT_EVAL_TAGGED("no tag",       "def", "def", ":* *=<*2,*1>", "[DEF] <tag,no>");
            TEST_EXPECT_EVAL_TAGGED("[no tag",      "def", "def", ":no=yes",      "[DEF] {yes tag");
            TEST_EXPECT_EVAL_TAGGED("no tag",       "def", "def", ":no=yes",      "[DEF] yes tag");
            TEST_EXPECT_EVAL_TAGGED("no tag",       "def", "DEF", ":no=yes",      "[DEF] yes tag");
            TEST_EXPECT_EVAL_TAGGED("no tag",       "DEF", "def", ":no=yes",      "[DEF] yes tag");
            TEST_EXPECT_EVAL_TAGGED("kept [trunk",  "def", "def", ":*=<*>",       "[DEF] <kept {trunk>");
            TEST_EXPECT_EVAL_TAGGED("kept",         "def", "def", ":*=<*>",       "[DEF] <kept>");
        }

        {
            GBDATA *gb_species = GBT_find_species(gb_main, "MetMazei");
            TEST_REJECT_NULL(gb_species);
            GBL_call_env callEnv(gb_species, env);

            // run scripts using context:
            TEST_EXPECT_EVAL_TAGGED("[T1,T2] name='$n'", "def", "T1", ":$n=*(name)",                              "[T1] name='MetMazei' [T2] name='$n'");
            TEST_EXPECT_EVAL_TAGGED("[T1,T2] seqlen=$l", "def", "T2", ":$l=*(|sequence|len)",                     "[T1] seqlen=$l [T2] seqlen=165");
            TEST_EXPECT_EVAL_TAGGED("[T1,T2] nuc",       "def", "T1", "dd;\"=\";command(sequence|count(ACGTUN))", "[T1] nuc=66 [T2] nuc");

            TEST_EXPECT_EVAL_TAGGED_ERROR_EXPORTED("tax='$t'", "def", "def", ":$t=*(|taxonomy(2))",  "Failed to read tree 'tree_missing' (Reason: tree not found)");
            TEST_EXPECT_EVAL_TAGGED_ERROR_EXPORTED("tax",      "def", "def", "dd;\"=\";taxonomy(2)", "Failed to read tree 'tree_missing' (Reason: tree not found)");

            // content before 1st tag:
            TEST_EXPECT_EVAL_TAGGED("untagged [tag] tagged", "def", "tag", ":g=G", "[DEF] untagged [TAG] taGGed");
            TEST_EXPECT_EVAL_TAGGED("         [tag] tagged", "def", "tag", ":g=G", "[TAG] taGGed");

            // test elimination of leading/trailing whitespace:
            TEST_EXPECT_EVAL_TAGGED("   untagged     ",                         "def", "def", ":g=G", "[DEF] untaGGed"); // untagged content
            TEST_EXPECT_EVAL_TAGGED("[tag]   tagged     ",                      "def", "tag", ":g=G", "[TAG] taGGed");
            TEST_EXPECT_EVAL_TAGGED("   [trail]   trail   [tag]   tagged     ", "def", "tag", ":g=G", "[TAG] taGGed [TRAIL] trail");

#define MIXED_TAGS "[tag] tag [tip,top] tiptop [xx,yy,zz] zzz"

            TEST_EXPECT_EVAL_TAGGED(MIXED_TAGS, "def", "tip", ":tip=top",    "[TAG] tag [TIP] toptop [TOP] tiptop [XX,YY,ZZ] zzz");
            TEST_EXPECT_EVAL_TAGGED(MIXED_TAGS, "def", "yy",  ":zzz=tiptop", "[TAG] tag [TIP,TOP,YY] tiptop [XX,ZZ] zzz");
            TEST_EXPECT_EVAL_TAGGED(MIXED_TAGS, "def", "top", ":tiptop=zzz", "[TAG] tag [TIP] tiptop [TOP,XX,YY,ZZ] zzz");
        }
    }
    GB_close(gb_main);
}

void TEST_log_action() {
    for (int stamped = 0; stamped<=1; ++stamped) {
        TEST_ANNOTATE(GBS_global_string("stamped=%i", stamped));
        {
            char *logged = GBS_log_action_to("comment", "action", stamped);
            if (stamped) {
                TEST_EXPECT_CONTAINS(logged, "comment\n");
                TEST_EXPECT_CONTAINS(logged, "action\n");
            }
            else {
                TEST_EXPECT_EQUAL(logged, "comment\naction\n");
            }
            free(logged);
        }
        {
            char *logged = GBS_log_action_to("comment\n", "action", stamped);
            if (stamped) {
                TEST_EXPECT_CONTAINS(logged, "comment\n");
                TEST_EXPECT_CONTAINS(logged, "action\n");
            }
            else {
                TEST_EXPECT_EQUAL(logged, "comment\naction\n");
            }
            free(logged);
        }
        {
            char *logged = GBS_log_action_to("", "action", stamped);
            if (stamped) {
                TEST_EXPECT_EQUAL(logged[0], '\n');
                TEST_EXPECT_CONTAINS(logged, "action\n");
            }
            else {
                TEST_EXPECT_EQUAL(logged, "\naction\n");
            }
            free(logged);
        }
        {
            char *logged = GBS_log_action_to(NULp, "action\n", stamped); // test action with trailing LF
            if (stamped) {
                TEST_EXPECT_DIFFERENT(logged[0], '\n');
                TEST_EXPECT_CONTAINS(logged, "action\n");
            }
            else {
                TEST_EXPECT_EQUAL(logged, "action\n");
            }
            free(logged);
        }
    }
}
TEST_PUBLISH(TEST_log_action);

#endif // UNIT_TESTS

