// ========================================================= //
//                                                           //
//   File      : ConfigMapping.cxx                           //
//   Purpose   : config <-> string mapping                   //
//                                                           //
//   Coded by Ralf Westram (coder@reallysoft.de) in Mar 19   //
//   http://www.arb-home.de/                                 //
//                                                           //
// ========================================================= //

#include "ConfigMapping.h"
#include <arb_msg.h>


using namespace std;

GB_ERROR ConfigMapping::decode_escapes(string& s) {
    string::iterator f = s.begin();
    string::iterator t = s.begin();

    for (; f != s.end(); ++f, ++t) {
        if (*f == '\\') {
            ++f;
            if (f == s.end()) return GBS_global_string("Trailing \\ in '%s'", s.c_str());
            switch (*f) {
                case 'n':
                    *t = '\n';
                    break;
                case 'r':
                    *t = '\r';
                    break;
                case 't':
                    *t = '\t';
                    break;
                default:
                    *t = *f;
                    break;
            }
        }
        else {
            *t = *f;
        }
    }

    s.erase(t, f);

    return NULp;
}

void ConfigMapping::encode_escapes(string& s, const char *to_escape) {
    string neu;
    neu.reserve(s.length()*2+1);

    for (string::iterator p = s.begin(); p != s.end(); ++p) {
        if (*p == '\\' || strchr(to_escape, *p)) {
            neu = neu+'\\'+*p;
        }
        else if (*p == '\n') { neu = neu+"\\n"; }
        else if (*p == '\r') { neu = neu+"\\r"; }
        else if (*p == '\t') { neu = neu+"\\t"; }
        else { neu = neu+*p; }
    }
    s = neu;
}

GB_ERROR ConfigMapping::parseFrom(const std::string& configString) {
    // parse string in format "key1='value1';key2='value2'"..
    // and put values into a map.
    //
    // assumes that keys are unique

    size_t   pos         = 0;
    GB_ERROR parse_error = NULp;

    while (!parse_error) {
        size_t equal = configString.find('=', pos);
        if (equal == string::npos) break;

        const char lookingAt = configString[equal+1];
        if (lookingAt != '\'') {
            parse_error = GBS_global_string("expected quote \"'\" after \"=\", found \"%c\"", lookingAt);
            break;
        }
        size_t start = equal+2;
        size_t end   = configString.find('\'', start);
        while (end != string::npos) {
            if (configString[end-1] != '\\') break; // unescaped quote (=end of value)
            if (configString[end-2] == '\\') { // multiple escapes -> count
                int    escCount = 2;
                size_t bwd      = end-3;
                while (bwd>=start && configString[bwd] == '\\') {
                    escCount++;
                    bwd--;
                }
                if ((escCount%2) == 0) break; // even number of escapes => unescaped quote (=end of value)
                // otherwise the quote belongs to the value
            }
            end = configString.find('\'', end+1);
        }
        if (end == string::npos) {
            parse_error = "could not find matching quote \"'\"";
            break;
        }

        string config_name = configString.substr(pos, equal-pos);
        string value       = configString.substr(start, end-start);

        parse_error = decode_escapes(value);
        if (!parse_error) {
            set_entry(config_name, value);
        }

        pos = end+2; // skip ';'
    }
    return parse_error;
}

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

#ifdef UNIT_TESTS
#ifndef TEST_UNIT_H
#include <test_unit.h>
#include <arb_defs.h>
#endif

#define TEST_ESCAPE_ENCODING(plain,expected) do{                        \
        string encoded = plain;                                         \
        ConfigMapping::encode_escapes(encoded, "'");                    \
        TEST_EXPECT_EQUAL(encoded, expected);                           \
        string decoded = encoded;                                       \
        TEST_EXPECT_NO_ERROR(ConfigMapping::decode_escapes(decoded));   \
        TEST_EXPECT_EQUAL(decoded, plain);                              \
    }while(0)

void TEST_configValueEscaping() {
    TEST_ESCAPE_ENCODING("hello", "hello"); // plain text

    TEST_ESCAPE_ENCODING("'hello'",   "\\'hello\\'"); // quoted text
    TEST_ESCAPE_ENCODING("\"hello\"", "\"hello\"");   // double-quoted text

    // special characters (LF, CR + TAB shall not be saved to config, esp. if saving to file via AWT_config_manager)
    TEST_ESCAPE_ENCODING("LINE\nNEXT", "LINE\\nNEXT");
    TEST_ESCAPE_ENCODING("1\t2\r3",    "1\\t2\\r3");

    // simple escape-handling
    TEST_ESCAPE_ENCODING("hel\\lo", "hel\\\\lo");
    TEST_ESCAPE_ENCODING("\\hello", "\\\\hello");
    TEST_ESCAPE_ENCODING("hello\\", "hello\\\\");

    TEST_ESCAPE_ENCODING("hello\\'",    "hello\\\\\\'");
    TEST_ESCAPE_ENCODING("\\'hello\\'", "\\\\\\'hello\\\\\\'");

    string invalidEncoding = "xyz\\";
    TEST_EXPECT_ERROR_CONTAINS(ConfigMapping::decode_escapes(invalidEncoding), "Trailing \\ in 'xyz\\'");
}

struct TestedConfig {
    const char *configString;
    int         entryCount;
    const char *entryList;
};
struct ReinterpretedConfig {
    const char *configString;
    const char *reinterpreted;
};
struct FailingConfig {
    const char *configString;
    GB_ERROR    error;
};

static TestedConfig testedCfg[] = {
    { "",                      0, ""},               // empty config
    { "tag='value'",           1, "tag" },
    { "321='1st';abc='2nd'",   2, "321/abc" },
    { "t-ha t='2';t;hi/s='1'", 2, "t-ha t,t;hi/s" }, // tags can contain anything but '='
    { "t'ag'='value'",         1, "t'ag'" },         // even quotes are possible in tags
};
static FailingConfig failedCfg[] = {
    { "nix=;was='ia'", "expected quote \"'\"" },          // no value
    { "tag=value",     "expected quote \"'\"" },          // unquoted value
    { "hasto='match",  "could not find matching quote" }, // unmatched quote
};
static ReinterpretedConfig reinterpretCfg[] = {
    { "laksjd",            "" },                  // is reinterpreted as "" (empty config)
    { "this='1';that='2'", "that='2';this='1'" }, // sorted by tagname
};

inline bool anyStringContains(const CharPtrArray& strings, char c) {
    for (int i = 0; strings[i]; ++i) {
        if (strchr(strings[i], c)) {
            return true;
        }
    }
    return false;
}
static char autodetectSeparator(const CharPtrArray& strings) {
    const char *sep = "/;,:|#*";
    for (int i = 0; sep[i]; ++i) {
        if (!anyStringContains(strings, sep[i])) {
            return sep[i];
        }
    }
    TEST_REJECT(true); // add more sep
    return 0;
}

void TEST_ConfigMapping() {
    for (size_t c = 0; c<ARRAY_ELEMS(testedCfg); ++c) {
        const TestedConfig& CFG = testedCfg[c];
        TEST_ANNOTATE(CFG.configString);

        ConfigMapping config;
        TEST_EXPECT_NO_ERROR(config.parseFrom(CFG.configString));

        // convert to string + compare with loaded config string:
        TEST_EXPECT_EQUAL(config.config_string(), CFG.configString);

        // test entries:
        ConstStrArray entries;
        config.get_entries(entries);
        TEST_EXPECT_EQUAL(entries.size(), CFG.entryCount);

        TEST_EXPECT_EQUAL_STRINGCOPY__NOERROREXPORTED(GBT_join_strings(entries, autodetectSeparator(entries)), CFG.entryList);
    }

    // test nested quoting:
    {
        TEST_ANNOTATE(NULp);
        const char    *stored = "tag='val';'qtag'='\\'qval\\''";
        ConfigMapping  config;
        TEST_EXPECT_NO_ERROR(config.parseFrom(stored));

        TEST_EXPECT_EQUAL(config.get_entry("tag"),    "val");
        TEST_EXPECT_EQUAL(config.get_entry("'qtag'"), "'qval'");

        TEST_EXPECT(config.has_entry("'qtag'"));
        TEST_REJECT(config.has_entry("qtag"));

        const char    *storedAsValue = "stored='tag=\\'val\\';\\'qtag\\'=\\'\\\\\\'qval\\\\\\'\\''";
        ConfigMapping  wrapped;
        wrapped.set_entry("stored", stored);

        TEST_EXPECT_EQUAL(wrapped.config_string(), storedAsValue);

        ConfigMapping unwrapped;
        TEST_EXPECT_NO_ERROR(unwrapped.parseFrom(storedAsValue));
        TEST_EXPECT_EQUAL(unwrapped.get_entry("stored"), stored);

        // test delete entry
        unwrapped.delete_entry("stored");
        TEST_EXPECT_EQUAL(unwrapped.config_string(), "");

        config.delete_entry("'qtag'");
        TEST_EXPECT_EQUAL(config.config_string(), "tag='val'");

        config.set_entry("tag", "lav"); // test overwriting a value
        TEST_EXPECT_EQUAL(config.config_string(), "tag='lav'");

        const char *SLASHED = "slashed\\";
        config.set_entry("key", SLASHED);
        TEST_EXPECT_EQUAL(config.config_string(), "key='slashed\\\\';tag='lav'");

        {
            ConfigMapping slashed;
            string        cfgStr = config.config_string();

            TEST_EXPECT_NO_ERROR(slashed.parseFrom(cfgStr));
            TEST_EXPECT_EQUAL(slashed.get_entry("key"), SLASHED);
            TEST_EXPECT_EQUAL(slashed.config_string(), cfgStr);
        }
    }

    // test reinterpretation of configs:
    for (size_t c = 0; c<ARRAY_ELEMS(reinterpretCfg); ++c) {
        const ReinterpretedConfig& CFG = reinterpretCfg[c];
        TEST_ANNOTATE(CFG.configString);
        ConfigMapping config;
        TEST_EXPECT_NO_ERROR(config.parseFrom(CFG.configString));

        // convert to string + compare with expected reinterpretation:
        TEST_EXPECT_EQUAL(config.config_string(), CFG.reinterpreted);
    }

    // test error-config:
    for (size_t c = 0; c<ARRAY_ELEMS(failedCfg); ++c) {
        const FailingConfig& CFG = failedCfg[c];
        TEST_ANNOTATE(CFG.configString);
        ConfigMapping config;
        TEST_EXPECT_ERROR_CONTAINS(config.parseFrom(CFG.configString), CFG.error);
    }
}

#endif // UNIT_TESTS

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

