// ========================================================= //
//                                                           //
//   File      : saiop.cxx                                   //
//   Purpose   : operations on SAI                           //
//                                                           //
//   Coded by Ralf Westram (coder@reallysoft.de) in Oct 19   //
//   http://www.arb-home.de/                                 //
//                                                           //
// ========================================================= //

#include "saiop.h"

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

using namespace std;

// --------------------
//      SaiCalcEnv

GB_ERROR SaiCalcEnv::check_lengths_equal(size_t& len) const { // @@@ also use in SaiAciApplicator
    if (input.empty()) {
        return "missing input data";
    }
    len = strlen(input[0]);
    for (unsigned i = 1; i<input.size(); ++i) {
        size_t otherLen = strlen(input[i]);
        if (otherLen != len) {
            return GBS_global_string("length mismatch in input data (%zu <> %zu)", len, otherLen);
        }
    }
    return NULp;
}

// ---------------------
//      SaiOperator

const char *SaiOperator::typeName[] = {
    "ACI",
    "Translator",
    "Matrix",
    "Bool chain",
};

ErrorOrSaiOperatorPtr SaiOperator::make(SaiOperatorType type, const char *config) {
    switch (type) {
        case SOP_TRANSLATE: return SaiTranslator::make(config);
        case SOP_MATRIX:    return SaiMatrixTranslator::make(config);
        case SOP_BOOLCHAIN: return SaiBoolchainOperator::make(config);
        case SOP_ACI:       return SaiAciApplicator::make(config);
    }
    return ErrorOrSaiOperatorPtr("can't make SaiOperator of that type (yet)", SaiOperatorPtr());
}

string SaiOperator::get_description() const {
    string desc = type_name(get_type());

    desc += ": ";
    desc += get_config(); // append config string

    return desc;
}

// -----------------------
//      SaiTranslator

void SaiTranslator::addTranslation(const char *from, char to) {
    for (size_t o = 0; from[o]; ++o) {
        transtab[safeCharIndex(from[o])] = to;
    }
}

ErrorOrString SaiTranslator::apply(const SaiCalcEnv& calcEnv) const {
    ARB_ERROR error;
    string    result;

    const CharPtrArray& input = calcEnv.get_input();
    if (input.size() != 1) {
        error = GBS_global_string("translator applies to single SAI only (have: %zu)", input.size());
    }
    else {
        const char *in     = input[0];
        size_t      length = strlen(in);

        result.resize(length);
        for (size_t o = 0; o<length; ++o) {
            result[o] = transtab[safeCharIndex(in[o])];
        }
    }
    return ErrorOrString(error, result);
}

void SaiTranslator::deduceTranslations(class ConfigMapping& mapping) const {
    // count occurrences of target characters in 'transtab'
    uint8_t count[256];
    for (int i = 0; i<256; ++i) count[i] = 0;
    for (int i = 1; i<256; ++i) ++count[safeCharIndex(transtab[i])];

    // detect default translation (=max used)
    int           maxCount           = 0;
    unsigned char defaultTranslation = 0;
    for (int i = 1; i<256; ++i) {
        if (count[i]>maxCount) {
            maxCount           = count[i];
            defaultTranslation = (unsigned char)i;
        }
    }

    mapping.set_entry("default", GBS_global_string("%c", defaultTranslation));

    int transCount = 0;
    for (int i = 1; i<256; ++i) {
        if (count[i]>0 && i != defaultTranslation) {
            unsigned char transTo   = (unsigned char)i;
            string        transFrom(1, transTo); // first character is target-char; rest are source-chars

            for (int j = 1; j<256; ++j) {
                if (transtab[j] == transTo) {
                    transFrom += (unsigned char)j;
                }
            }

            sai_assert(transFrom.length()>0);
            mapping.set_entry(GBS_global_string("trans%i", ++transCount), transFrom);
        }
    }
}

string SaiTranslator::get_config() const {
    ConfigMapping cfgmap;
    deduceTranslations(cfgmap);
    return cfgmap.config_string();
}

ErrorOrSaiOperatorPtr SaiTranslator::make(const char *config) {
    SaiOperatorPtr result;
    ConfigMapping  cfgmap;
    ARB_ERROR      error = cfgmap.parseFrom(config);

    if (!error) {
        const char *defTrans = cfgmap.get_entry("default");
        if (defTrans) {
            if (defTrans[0] && !defTrans[1]) { // expect exactly 1 char
                SaiTranslator *translator = new SaiTranslator(defTrans[0]);

                int transCount = 0;
                while (!error) {
                    const char *entry = GBS_global_string("trans%i", ++transCount);
                    const char *trans = cfgmap.get_entry(entry);
                    if (!trans) break;

                    if (trans[0] && trans[1]) {
                        translator->addTranslation(trans+1, trans[0]);
                    }
                    else {
                        error = GBS_global_string("invalid content '%s' in config entry '%s'", trans, entry);
                    }
                }

                if (error) {
                    delete translator;
                }
                else {
                    result = translator;
                }
            }
            else {
                error = GBS_global_string("invalid content '%s' in config entry 'default'", defTrans);
            }
        }
        else {
            error = "missing 'default' entry";
        }
    }

    return ErrorOrSaiOperatorPtr(error, result);
}

// -----------------------------
//      SaiMatrixTranslator

std::string SaiMatrixTranslator::get_config() const {
    ConfigMapping cfgmap;
    cfgmap.set_entry("first", firstToIndexChar->get_config());
    cfgmap.set_entry("columns", GBS_global_string("%zu", secondToResult.size()));
    for (size_t s = 0; s<secondToResult.size(); ++s) {
        string key = GBS_global_string("col%zu", s);
        cfgmap.set_entry(key, secondToResult[s]->get_config());
    }
    return cfgmap.config_string();
}

ErrorOrString SaiMatrixTranslator::apply(const SaiCalcEnv& calcEnv) const {
    ARB_ERROR error;
    string    result;

    const CharPtrArray& input = calcEnv.get_input();
    if (input.size() != 2) {
        error = GBS_global_string("matrix translator applies to a pair of SAIs only (have: %zu)", input.size());
    }
    else {
        size_t length;
        error = calcEnv.check_lengths_equal(length); // fails if both SAIs do not match in length
        if (!error) {
            result.resize(length);

            const CharPtrArray& sai = calcEnv.get_input();

            // translate 1st sai using firstToIndexChar:
            ConstStrArray sai1;
            sai1.put(sai[0]);

            SaiCalcEnv env1(sai1, calcEnv.get_gbmain());

            ErrorOrString trans1 = firstToIndexChar->apply(env1);
            if (trans1.hasError()) {
                error = trans1.getError();
            }
            else {
                string       tindex   = trans1.getValue();   // index into 'secondToResult' (to select translator defined by matrix column)
                const size_t idxCount = secondToResult.size();

                vector<string> trans2;
                trans2.reserve(idxCount);

                // foreach entry in secondToResult -> translate 2nd sai using that entry (+store in array):
                {
                    ConstStrArray sai2;
                    sai2.put(sai[1]);

                    SaiCalcEnv env2(sai2, calcEnv.get_gbmain());

                    for (size_t i = 0; i<idxCount && !error; ++i) {
                        ErrorOrString t = secondToResult[i]->apply(env2);
                        if (t.hasError()) {
                            error = t.getError();
                        }
                        else {
                            trans2.push_back(t.getValue());
                        }
                    }
                }

                if (!error) {
                    sai_assert(trans2.size() == idxCount);

                    // iterate over sai positions and read result from translation "matrix":
                    for (size_t o = 0; o<length; ++o) {
                        int idx   = safeCharIndex(tindex[o]) - DEFAULT_INDEX_CHAR;
                        sai_assert(idx>=0 && size_t(idx)<idxCount);
                        result[o] = trans2[idx][o];
                    }
                }
            }
        }
    }
    return ErrorOrString(error, result);
}

void SaiMatrixTranslator::addOperator(const char *from, SaiOperatorPtr to) {
    size_t meta = secondToResult.size()+DEFAULT_INDEX_CHAR;
    sai_assert(meta < 256);
    dynamic_cast<SaiTranslator*>(&*firstToIndexChar)->addTranslation(from, char(meta));
    secondToResult.push_back(to);
}

ErrorOrSaiOperatorPtr SaiMatrixTranslator::make(const char *config) {
    SaiOperatorPtr result;
    ConfigMapping  cfgmap;
    ARB_ERROR      error = cfgmap.parseFrom(config);

    if (!error) {
        const char *first = cfgmap.get_entry("first");
        if (first) {
            ErrorOrSaiOperatorPtr product = SaiOperator::make(SOP_TRANSLATE, first);
            if (product.hasValue()) {
                SaiMatrixTranslator *smt = new SaiMatrixTranslator;
                smt->firstToIndexChar    = product.getValue(); // overwrite first translator

                const char *columnsStr = cfgmap.get_entry("columns");
                if (columnsStr) {
                    int columns = atoi(columnsStr);
                    if (columns>0) {
                        sai_assert(smt->secondToResult.size() == 0);

                        for (int c = 0; c<columns && !error; ++c) {
                            string      key = GBS_global_string("col%i", c);
                            const char *col = cfgmap.get_entry(key.c_str());
                            if (col) {
                                ErrorOrSaiOperatorPtr colProduct = SaiOperator::make(SOP_TRANSLATE, col);
                                if (colProduct.hasValue()) {
                                    smt->secondToResult.push_back(colProduct.getValue()); // (compare: addOperator)
                                }
                                else {
                                    error = GBS_global_string("%s (in '%s'; entry '%s')", colProduct.getError().deliver(), col, key.c_str());
                                }
                            }
                            else {
                                error = GBS_global_string("missing '%s' entry", key.c_str());
                            }
                        }
                    }
                    else {
                        error = GBS_global_string("entry 'columns' has to be 1 or higher (have: '%s')", columnsStr);
                    }
                }
                else {
                    error = "missing 'columns' entry";
                }

                result = smt;
            }
            else {
                error = GBS_global_string("%s (in '%s'; entry 'first')", product.getError().deliver(), first);
            }
        }
        else {
            error = "missing 'first' entry";
        }
    }
    return ErrorOrSaiOperatorPtr(error, result);
}

// ---------------------
//      SaiBoolRule

void SaiBoolRule::apply(char *inout, const char *in, size_t len) const {
    for (size_t i = 0; i<len; ++i) {
        bool a = inout[i]-'0';
        bool b = in[i]-'0';
        bool c = false;

        switch (op) {
            case SBO_FIRST: sai_assert(0); break; // cannot be applied
            case SBO_AND:  c = a && b;    break;
            case SBO_OR:   c = a || b;    break;
            case SBO_XOR:  c = a ^ b;     break;
            case SBO_NAND: c = !(a && b); break;
            case SBO_NOR:  c = !(a || b); break;
            case SBO_XNOR: c = !(a ^ b);  break;
        }

        inout[i] = c ? '1' : '0';
    }
}

static const char *opname[] = { // @@@ rename variable
    "-->",
    "AND",
    "OR",
    "XOR",
    "NAND",
    "NOR",
    "XNOR",
    NULp
};

string SaiBoolRule::to_string() const {
    // used to save into config AND used for display in rule selection list
    string       result = opname[op];
    const size_t MAXLEN = 4;

#if defined(ASSERTION_USED)
    const size_t rlen = result.length();
#endif

    sai_assert(rlen<=MAXLEN);

    // align charset definitions at same column (to beautify list display)
    for (size_t p = result.length(); p<(MAXLEN+1); ++p) {
        result += ' ';
    }

    result += specifyTrueChars ? '[' : ']';
    result += chars;
    result += specifyTrueChars ? ']' : '[';

    return result;
}

ErrorOrSaiBoolRulePtr SaiBoolRule::make(const char *fromString) {
    // convert 'fromString' created by to_string() back to SaiBoolRule
    SaiBoolRulePtr product;
    ARB_ERROR      error;

    if (!fromString) {
        error = "no input string";
    }
    else {
        const char *space1     = strchr(fromString, ' ');
        bool        appendFrom = true;

        if (!space1) {
            error = "expected at least one space character";
        }
        else {
            int tok1len = space1-fromString;

            int op;
            for (op = 0; opname[op]; ++op) {
                if (strncmp(opname[op], fromString, tok1len) == 0) {
                    break;
                }
            }

            if (!opname[op]) {
                char *tok = ARB_strpartdup(fromString, space1-1);
                error     = GBS_global_string("unknown operator token '%s'", tok);
                free(tok);
            }
            else {
                const char *rest = space1;
                while (rest[0] == ' ') ++rest; // eat all spaces

                if (!rest[0]) {
                    error = "truncated input string";
                }
                else {
                    bool specTrue = rest[0] == '[';
                    if (!specTrue && rest[0] != ']') {
                        error = GBS_global_string("Expected '[' or ']', found '%c'", rest[0]);
                    }
                    else {
                        char *chars   = strdup(rest+1);
                        int   lastPos = strlen(chars)-1;

                        if (lastPos<0) {
                            error = "character specification too short";
                        }
                        else {
                            char lastExpected = rest[0] == '[' ? ']' : '[';

                            if (chars[lastPos] != lastExpected) {
                                error = GBS_global_string("expected character specification to be terminated by '%c' (seen '%c')", lastExpected, chars[lastPos]);
                            }
                            else {
                                chars[lastPos] = 0; // truncate last char
                                product = new SaiBoolRule(SaiBoolOp(op), specTrue, chars);
                            }
                        }
                        free(chars);
                    }
                }
            }
        }

        if (error && appendFrom) {
            error = GBS_global_string("%s in '%s'", error.deliver(), fromString);
        }
    }

    return ErrorOrSaiBoolRulePtr(error, product);
}

static ErrorOrSaiBoolRulePtr makeFromConfigRule(const ConfigMapping& cfgmap, int ruleNr) {
    const char *rulename = GBS_global_string("rule%i", ruleNr);
    const char *rule     = cfgmap.get_entry(rulename);

    SaiBoolRulePtr noResult;
    if (!rule) {
        ARB_ERROR error = GBS_global_string("expected entry '%s' is missing", rulename);
        return ErrorOrSaiBoolRulePtr(error, noResult);
    }

    ErrorOrSaiBoolRulePtr result = SaiBoolRule::make(rule);
    if (result.hasError()) {
        ARB_ERROR error = GBS_global_string("%s (during production of 'rule%i')", result.getError().deliver(), ruleNr);
        return ErrorOrSaiBoolRulePtr(error, noResult);
    }

    return result;
}

// ------------------------------
//      SaiBoolchainOperator

std::string SaiBoolchainOperator::get_config() const {
    ConfigMapping cfgmap;
    cfgmap.set_entry("out", GBS_global_string("%c%c", outTrans[0], outTrans[1]));
    cfgmap.set_entry("rules", GBS_global_string("%zu", rule.size()));
    for (size_t r = 0; r<rule.size(); ++r) {
        string key = GBS_global_string("rule%zu", r);
        cfgmap.set_entry(key, rule[r].to_string());
    }
    return cfgmap.config_string();
}

ErrorOrString SaiBoolchainOperator::apply(const SaiCalcEnv& calcEnv) const {
    string    result;
    size_t    sailen;
    ARB_ERROR error = calcEnv.check_lengths_equal(sailen);

    if (!error) {
        const CharPtrArray& input = calcEnv.get_input();

        const size_t saiCount  = input.size();
        const size_t ruleCount = rule.size();

        if (ruleCount<1) {
            error = "need at least one rule in chain";
        }
        else if (saiCount != ruleCount) {
            error = GBS_global_string("number of input SAIs has to match number of rules (%zu <> %zu)", saiCount, ruleCount);
        }
        else {
            char buffer[sailen+1];
            rule[0].prepare_input_data(input[0], sailen, buffer);

            for (size_t r = 1; r<ruleCount; ++r) {
                char othBuf[sailen+1];
                rule[r].prepare_input_data(input[r], sailen, othBuf);
                rule[r].apply(buffer, othBuf, sailen);
            }

            // translate 01 into wanted output characters:
            for (size_t i = 0; i<sailen; ++i) {
                buffer[i] = outTrans[buffer[i]-'0'];
            }

            result = buffer;
        }
    }

    return ErrorOrString(error, result);
}

ErrorOrSaiOperatorPtr SaiBoolchainOperator::make(const char *config) {
    SaiOperatorPtr result;
    ConfigMapping  cfgmap;
    ARB_ERROR      error = cfgmap.parseFrom(config);

    if (!error) {
        const char *rulesStr = cfgmap.get_entry("rules");
        const char *out      = cfgmap.get_entry("out");

        if      (!rulesStr) error = "expected 'rules' entry missing";
        else if (!out)      error = "expected 'out' entry missing";
        else {
            int  rules = atoi(rulesStr);
            char out0  = out[0] ? out[0] : '-';
            char out1  = out[0] && out[1] ? out[1] : 'x';

            if (rules<1) { // no rule in config -> create default op
                result = new SaiBoolchainOperator(out0, out1);
            }
            else {
                ErrorOrSaiBoolRulePtr first = makeFromConfigRule(cfgmap, 0); // uses 'rule0'

                if (first.hasError()) {
                    error = first.getError();
                }
                else {
                    SaiBoolRulePtr rule = first.getValue();
                    if (rule->get_op() != SBO_FIRST) {
                        error = GBS_global_string("wrong type in 'rule0' (expected: '%s'; got: '%s')", opname[SBO_FIRST], opname[rule->get_op()]);
                    }
                    else {
                        SaiBoolchainOperator *sbo = new SaiBoolchainOperator(out0, out1);
                        sbo->addRule(*rule); // add 1st rule

                        // add other rules:
                        for (int r = 1; r<rules && !error; ++r) {
                            ErrorOrSaiBoolRulePtr next = makeFromConfigRule(cfgmap, r); // uses 'rule1' and following
                            if (next.hasError()) {
                                error = next.getError();
                            }
                            else {
                                rule = next.getValue();
                                if (rule->get_op() == SBO_FIRST) {
                                    error = GBS_global_string("wrong type in 'rule%i' (may not be '%s')", r, opname[SBO_FIRST]);
                                }
                                else {
                                    sbo->addRule(*rule);
                                }
                            }
                        }

                        result = sbo;
                    }
                }
            }
        }
    }

    return ErrorOrSaiOperatorPtr(error, result);
}

// --------------------------
//      SaiAciApplicator

std::string SaiAciApplicator::get_config() const {
    ConfigMapping cfgmap;
    cfgmap.set_entry("aci", aci);
    return cfgmap.config_string();
}

ErrorOrSaiOperatorPtr SaiAciApplicator::make(const char *config) {
    SaiOperatorPtr result;
    ConfigMapping  cfgmap;
    ARB_ERROR      error = cfgmap.parseFrom(config);

    if (!error) {
        const char *defAci = cfgmap.get_entry("aci");
        if (defAci) {
            SaiAciApplicator *aciOp = new SaiAciApplicator(defAci);
            result = aciOp;
        }
        else {
            error = "missing 'aci' entry";
        }
    }

    return ErrorOrSaiOperatorPtr(error, result);
}

ErrorOrString SaiAciApplicator::apply(const SaiCalcEnv& calcEnv) const {
    ARB_ERROR error;
    string    result;

    const CharPtrArray& input = calcEnv.get_input();

    size_t len[input.size()];
    size_t maxlen = 0;

    for (int i = 0; input[i]; ++i) {
        len[i] = strlen(input[i]);
        maxlen = std::max(len[i], maxlen);
    }

    if (maxlen == 0) {
        error = "no input data";
    }
    else {
        GBL_maybe_itemless_call_env callEnv(calcEnv.get_gbmain(), NULp);

        for (size_t p = 0; p<maxlen && !error; ++p) {
            string toAci;
            for (int i = 0; input[i]; ++i) {
                if (p<len[i]) {
                    toAci += input[i][p];
                }
            }

            char *fromAci = GB_command_interpreter_in_env(toAci.c_str(), aci.c_str(), callEnv);
            if (fromAci) {
                size_t fromAciLen  = strlen(fromAci);
                if (fromAciLen == 1) {
                    result += fromAci;
                }
                else {
                    error = GBS_global_string("Expected single character result from ACI (but received %zu chars; while '%s' -> '%s')",
                                              fromAciLen, toAci.c_str(), fromAci);
                }
                free(fromAci);
            }
            else {
                error = GB_await_error();
            }
        }
    }

    return ErrorOrString(error, result);
}

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

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

#define TEST_EXPECT_APPLY_RESULT(op,env,expected) do{                           \
        ErrorOrString result = (op)->apply(env);                                \
        TEST_EXPECT(result.hasValue());                                         \
        string output = result.getValue();                                      \
        TEST_EXPECT_EQUAL(output, expected);                                    \
        TEST_EXPECT_EQUAL(output.length(), strlen((env).get_input()[0]));       \
    }while(0)


#define TEST_EXPECT_APPLY_FAILURE(op,env,errorPart) do{                 \
        ErrorOrString result = (op)->apply(env);                        \
        TEST_EXPECT(result.hasError());                                 \
        TEST_EXPECT_CONTAINS(result.getError().deliver(), errorPart);   \
    }while(0)

#define TEST_OPERATOR_PRODUCTION_FAILS(optype,cfg,errorPart) do{        \
        ErrorOrSaiOperatorPtr product = SaiOperator::make(optype, cfg); \
        TEST_EXPECT(product.hasError());                                \
        TEST_EXPECT_CONTAINS(product.getError().deliver(), errorPart);  \
    }while(0)


// TEST_OP_CFG_CONV_BIJECTIVE does
// - create + test config from 'op'
// - ask factory to re-create operator from config
// - test reloaded operator (has same type, produces same config and operates the same result when applied to 'env')
// (Note: TEST_OP_CFG_CONV_BIJECTIVE_SIMPLE doesn't apply)

#define TEST_OCCB_COMMONCODE(op,cfgExpected)                                            \
    const string cfg = (op)->get_config();                                              \
    TEST_EXPECT_EQUAL(cfg, cfgExpected);                                                \
    ErrorOrSaiOperatorPtr product = SaiOperator::make((op)->get_type(), cfg.c_str());   \
    TEST_EXPECT(product.hasValue());                                                    \
    SaiOperatorPtr op_reloaded = product.getValue();                                    \
    TEST_EXPECT_EQUAL(op_reloaded->get_type(), (op)->get_type());                       \
    const string cfg_reloaded = op_reloaded->get_config();                              \
    TEST_EXPECT_EQUAL(cfg_reloaded, cfg)

#define TEST_OP_CFG_CONV_BIJECTIVE_SIMPLE(op,cfgExpected) do{   \
        TEST_OCCB_COMMONCODE(op,cfgExpected);                   \
    }while(0)

#define TEST_OP_CFG_CONV_BIJECTIVE(op,cfgExpected,env) do{                                              \
        TEST_OCCB_COMMONCODE(op,cfgExpected);                                                           \
        ErrorOrString result = (op)->apply(env);                                                        \
        ErrorOrString result_reloaded = op_reloaded->apply(env);                                        \
        TEST_EXPECT_EQUAL(result.hasValue(), result_reloaded.hasValue());                               \
        if (result.hasValue()) TEST_EXPECT_EQUAL(result.getValue(), result_reloaded.getValue());        \
        else TEST_EXPECT_EQUAL(result.getError().deliver(), result_reloaded.getError().deliver());      \
    }while(0)

void TEST_SaiTranslator() {
    SaiOperatorPtr op;

    // test simple translator
    SaiTranslator *st1 = new SaiTranslator('-');
    op                 = st1;

    TEST_EXPECT_EQUAL(op->get_type(), SOP_TRANSLATE);

    ConstStrArray input;
    input.put("[[..]]]]....]>>>>>].]>>>>].[<<[..[<<[....]>>]");

    SaiCalcEnv env(input, NULp); // we can fake gb_main here

    // apply operator + test results:
    TEST_EXPECT_APPLY_RESULT(op, env, "---------------------------------------------");

    // test some real translation:
    st1->addTranslation("]>", ')');
    st1->addTranslation("<[", '(');

    // ------------------------------ "[[..]]]]....]>>>>>].]>>>>].[<<[..[<<[....]>>]"
    TEST_EXPECT_APPLY_RESULT(op, env, "((--))))----)))))))-))))))-((((--((((----))))");

    // provoke apply-failure:
    input.put("whatever"); // provokes: too many input streams for translator
    TEST_EXPECT_APPLY_FAILURE(op, env, "translator applies to single SAI only (have: 2)"); // @@@ clumsy message; test for no SAI is in calculator.cxx@CLUMSYMSG
    input.remove(1); // again remove 'whatever'

    // test conversion op->config->op + test both ops operate identical:
    TEST_OP_CFG_CONV_BIJECTIVE(op, "default='-';trans1='(<[';trans2=')>]'", env);

    // try to reload some invalid configs (test error cases):
    TEST_OPERATOR_PRODUCTION_FAILS(SOP_TRANSLATE, "",                       "missing 'default' entry");
    TEST_OPERATOR_PRODUCTION_FAILS(SOP_TRANSLATE, "default='xxx'",          "invalid content 'xxx' in config entry 'default'");
    TEST_OPERATOR_PRODUCTION_FAILS(SOP_TRANSLATE, "default='x';trans1='z'", "invalid content 'z' in config entry 'trans1'");
}

void TEST_SaiMatrixTranslator() {
    SaiOperatorPtr op;

    // test simple matrix translator
    SaiTranslator       *st1  = new SaiTranslator('.');
    SaiMatrixTranslator *smt1 = new SaiMatrixTranslator(st1);
    op = smt1;

    TEST_EXPECT_EQUAL(op->get_type(), SOP_MATRIX);

    // create + restore default config:
    TEST_OP_CFG_CONV_BIJECTIVE_SIMPLE(op, "col0='default=\\'.\\'';columns='1';first='default=\\'A\\''");

    // helix numbers:      1                   2      2         3                 3                4    5       5      4        1
    const char *h1235 = "..[.<<<...<<<<<[......[<<[...]>>]......[<<<<..<...<<[....]>>....>.>>>].........[<<[....]>>>]..........]>>>>.>>>>].";
    const char *h1345 = "..[.<<<...<<<<[........................[<.<<..<<<<<<[....]>>>>>>>.>>.]....[<[..[<<<[...]>>]...]>]......]>>>>.>>>].";
    const char *res01 = "..[.<<<...<<<<<[......[<<[...]>>]......[<<<<..<<<<<<[....]>>>>>>>.>>>]....[<[..[<<<[...]>>>]..]>].....]>>>>>>>>>].";

    ConstStrArray input;
    input.put(h1235);

    SaiCalcEnv env(input, NULp); // we can fake gb_main here

    // matrix translator expects two SAIs:
    TEST_EXPECT_APPLY_FAILURE(op, env, "matrix translator applies to a pair of SAIs only (have: 1)");

    input.put("hello"); // now add second SAI string which is too short

    // matrix translator expects SAIs with same length:
    TEST_EXPECT_APPLY_FAILURE(op, env, "length mismatch in input data (114 <> 5)");

    input.remove(1);  // drop "too short" sai again
    input.put(h1345); // now add second SAI string

    struct {
        const char *from;
        const char *cfg;
    } columnTranslatorConfig[] = {
        { ".-=", "default='?';trans1='..-=';trans2='[[';trans3=']]';trans4='<<';trans5='>>'" },
        { "[",   "default='?';trans1='[[.-=';trans2='<<'" },
        { "]",   "default='?';trans1=']].-=';trans2='>>'" },
        { "<",   "default='?';trans1='<<.-=['" },
        { ">",   "default='?';trans1='>>.-=]'" },
        { NULp, NULp }
    };

    for (int t = 0; columnTranslatorConfig[t].from; ++t) {
        ErrorOrSaiOperatorPtr generated = SaiTranslator::make(columnTranslatorConfig[t].cfg);
        TEST_REJECT(generated.hasError());

        SaiOperatorPtr translator = generated.getValue();
        smt1->addOperator(columnTranslatorConfig[t].from, translator);
    }

    // apply operator + test results:
    TEST_EXPECT_APPLY_RESULT(op, env, res01);

    // create + test config
    TEST_OP_CFG_CONV_BIJECTIVE(op,
                               "col0='default=\\'.\\'';"
                               "col1='default=\\'?\\';trans1=\\'.-.=\\';trans2=\\'<<\\';trans3=\\'>>\\';trans4=\\'[[\\';trans5=\\']]\\'';"
                               "col2='default=\\'?\\';trans1=\\'<<\\';trans2=\\'[-.=[\\'';"
                               "col3='default=\\'?\\';trans1=\\'>>\\';trans2=\\']-.=]\\'';"
                               "col4='default=\\'?\\';trans1=\\'<-.<=[\\'';"
                               "col5='default=\\'?\\';trans1=\\'>-.=>]\\'';"
                               "columns='6';"
                               "first='default=\\'A\\';trans1=\\'B-.=\\';trans2=\\'C[\\';trans3=\\'D]\\';trans4=\\'E<\\';trans5=\\'F>\\''",
                               env);

    // try to reload some invalid configs (test error cases):
    TEST_OPERATOR_PRODUCTION_FAILS(SOP_MATRIX, "",                                                           "missing 'first' entry");
    TEST_OPERATOR_PRODUCTION_FAILS(SOP_MATRIX, "first=''",                                                   "missing 'default' entry (in ''; entry 'first')");
    TEST_OPERATOR_PRODUCTION_FAILS(SOP_MATRIX, "first='default=\\'\\''",                                     "invalid content '' in config entry 'default' (in 'default='''; entry 'first')");
    TEST_OPERATOR_PRODUCTION_FAILS(SOP_MATRIX, "first='default=\\'x\\''",                                    "missing 'columns' entry");
    TEST_OPERATOR_PRODUCTION_FAILS(SOP_MATRIX, "first='default=\\'x\\'';columns='0'",                        "entry 'columns' has to be 1 or higher (have: '0')");
    TEST_OPERATOR_PRODUCTION_FAILS(SOP_MATRIX, "first='default=\\'x\\'';columns='1'",                        "missing 'col0' entry");
    TEST_OPERATOR_PRODUCTION_FAILS(SOP_MATRIX, "first='default=\\'x\\'';columns='1';col0=''",                "missing 'default' entry (in ''; entry 'col0')");
    TEST_OPERATOR_PRODUCTION_FAILS(SOP_MATRIX, "first='default=\\'x\\'';columns='2';col0='default=\\'\\''",  "invalid content '' in config entry 'default' (in 'default='''; entry 'col0')");
    TEST_OPERATOR_PRODUCTION_FAILS(SOP_MATRIX, "first='default=\\'x\\'';columns='2';col0='default=\\'y\\''", "missing 'col1' entry");
}

void TEST_SaiBoolchainOperator() {
    const char *inp1 = "--xX";
    const char *inp2 = "-x=x";

    ConstStrArray input;
    SaiCalcEnv    env(input, NULp); // we can fake gb_main here

    // test simple boolchain operator
    SaiBoolchainOperator *sbco1 = new SaiBoolchainOperator('-', 'x');
    SaiOperatorPtr        op    = sbco1;
    TEST_EXPECT_EQUAL(op->get_type(), SOP_BOOLCHAIN);

    TEST_EXPECT_APPLY_FAILURE(op, env, "missing input data"); // applying operator to no data does fail

    input.put(inp1);

    TEST_EXPECT_APPLY_FAILURE(op, env, "need at least one rule in chain"); // applying operator w/o rules does fail
    TEST_OP_CFG_CONV_BIJECTIVE(op, "out='-x';rules='0'", env); // create + restore config w/o rules

    sbco1->addRule(SaiBoolRule(SBO_FIRST, true, "xX"));

    TEST_EXPECT_APPLY_RESULT(op, env, "--xx"); // apply single link boolchain to single SAI (=plain bool translation)
    TEST_OP_CFG_CONV_BIJECTIVE(op, "out='-x';rule0='-->  [xX]';rules='1'", env); // test create + restore config

    input.put(inp2);

    TEST_EXPECT_APPLY_FAILURE(op, env, "number of input SAIs has to match number of rules (2 <> 1)"); // apply single link boolchain to 2 SAIs and test failure

    struct {
        SaiBoolOp   op;
        const char *expected;
    } testdata[] = {
        // Input:   "--xx"
        //          "-x-x"
        { SBO_AND,  "---x" },
        { SBO_OR,   "-xxx" },
        { SBO_XOR,  "-xx-" },
        { SBO_NAND, "xxx-" },
        { SBO_NOR,  "x---" },
        { SBO_XNOR, "x--x" },
        { SBO_XOR,  "xx--" }, // [6] simulate a NOT (performs XOR "xxxx"; tests empty charset, see below)
        { SBO_FIRST,  NULp },
    };

    for (int d = 0; testdata[d].expected; ++d) {
        TEST_ANNOTATE(GBS_global_string("d=%i", d));
        sbco1->addRule(SaiBoolRule(testdata[d].op, false, d == 6 ? "" : "-="));
        TEST_EXPECT_APPLY_RESULT(op, env, testdata[d].expected);

        if (d == 3) {
            TEST_OP_CFG_CONV_BIJECTIVE(op, "out='-x';rule0='-->  [xX]';rule1='NAND ]-=[';rules='2'", env);
        }

        sbco1->dropRule();
    }

    // test bool chains with 3 links:
    input.clear();

    input.put("-x-x-x-x");
    input.put("xx--xx--");
    input.put("xxxx----");

    struct {
        SaiBoolOp   op1, op2;
        const char *expected;
    } testdata2[] = {
        // Input:               "-x-x-x-x"
        //                      "xx--xx--"
        //                      "xxxx----"
        { SBO_AND,   SBO_AND,   "-x------" },
        { SBO_AND,   SBO_OR,    "xxxx-x--" },
        { SBO_OR,    SBO_AND,   "xx-x----" },
        { SBO_OR,    SBO_OR,    "xxxxxx-x" },
        { SBO_NOR,   SBO_XNOR,  "--x-xx-x" },
        { SBO_XNOR,  SBO_NAND,  "x--xxxxx" },
        { SBO_NAND,  SBO_XOR,   "-x--x-xx" },
        { SBO_XOR,   SBO_NOR,   "-----xx-" },
        { SBO_FIRST, SBO_FIRST, NULp },
    };

    for (int d = 0; testdata2[d].expected; ++d) {
        TEST_ANNOTATE(GBS_global_string("d=%i", d));
        sbco1->addRule(SaiBoolRule(testdata2[d].op1, false, "-="));
        sbco1->addRule(SaiBoolRule(testdata2[d].op2, false, "-="));
        TEST_EXPECT_APPLY_RESULT(op, env, testdata2[d].expected);

        if (d == 6) {
            TEST_OP_CFG_CONV_BIJECTIVE(op, "out='-x';rule0='-->  [xX]';rule1='NAND ]-=[';rule2='XOR  ]-=[';rules='3'", env);
        }

        sbco1->dropRule();
        sbco1->dropRule();
    }
    TEST_ANNOTATE(NULp);

    // try to reload some invalid configs (test error cases):
    TEST_OPERATOR_PRODUCTION_FAILS(SOP_BOOLCHAIN, "",                   "expected 'rules' entry missing");
    TEST_OPERATOR_PRODUCTION_FAILS(SOP_BOOLCHAIN, "rules='0'",          "expected 'out' entry missing");
    TEST_OPERATOR_PRODUCTION_FAILS(SOP_BOOLCHAIN, "rules='1';out=' +'", "expected entry 'rule0' is missing");

    TEST_OPERATOR_PRODUCTION_FAILS(SOP_BOOLCHAIN, "rules='1';out=' +';rule0='123'",      "expected at least one space character in '123' (during production of 'rule0')");
    TEST_OPERATOR_PRODUCTION_FAILS(SOP_BOOLCHAIN, "rules='1';out=' +';rule0='1 3'",      "unknown operator token '1' in '1 3' (during production of 'rule0')");

    TEST_OPERATOR_PRODUCTION_FAILS(SOP_BOOLCHAIN, "rules='1';out=' +';rule0='XOR  '",    "truncated input string in 'XOR  ' (during production of 'rule0')");
    TEST_OPERATOR_PRODUCTION_FAILS(SOP_BOOLCHAIN, "rules='1';out=' +';rule0='OR   3'",   "Expected '[' or ']', found '3' in 'OR   3' (during production of 'rule0')");
    TEST_OPERATOR_PRODUCTION_FAILS(SOP_BOOLCHAIN, "rules='1';out=' +';rule0='AND  ['",   "character specification too short in 'AND  [' (during production of 'rule0')");
    TEST_OPERATOR_PRODUCTION_FAILS(SOP_BOOLCHAIN, "rules='1';out=' +';rule0='XNOR []'",  "wrong type in 'rule0' (expected: '-->'; got: 'XNOR')"); // accepts empty charspec (can be used as NOT operator)
    TEST_OPERATOR_PRODUCTION_FAILS(SOP_BOOLCHAIN, "rules='1';out=' +';rule0='OR   ]x '", "expected character specification to be terminated by '[' (seen ' ') in 'OR   ]x ' (during production of 'rule0')");

    TEST_OPERATOR_PRODUCTION_FAILS(SOP_BOOLCHAIN, "rules='1';out=' +';rule0='NOR  [x]'",                  "wrong type in 'rule0' (expected: '-->'; got: 'NOR')");
    TEST_OPERATOR_PRODUCTION_FAILS(SOP_BOOLCHAIN, "rules='1';out=' +';rule0='NAND ]x['",                  "wrong type in 'rule0' (expected: '-->'; got: 'NAND')");
    TEST_OPERATOR_PRODUCTION_FAILS(SOP_BOOLCHAIN, "rules='2';out=' +';rule0='-->  ]x['",                  "expected entry 'rule1' is missing");
    TEST_OPERATOR_PRODUCTION_FAILS(SOP_BOOLCHAIN, "rules='2';out=' +';rule0='-->  ]x[';rule1='-->  ]x['", "wrong type in 'rule1' (may not be '-->')");
}

void TEST_SaiAciApplicator() {
    GB_shell  shell;
    GBDATA   *gb_main = GB_open("no.arb", "c");

    SaiOperatorPtr op;

    // test aci applicator
    const char       *aci  = "minus(1)|head(1)";
    SaiAciApplicator *saa1 = new SaiAciApplicator(aci);
    op                     = saa1;

    TEST_EXPECT_EQUAL(op->get_type(), SOP_ACI);

    ConstStrArray input;
    input.put("....664--2662440-44-61662664462-----4--4------662440...."); // some PVP

    SaiCalcEnv env(input, gb_main);

    {
        SaiAciApplicator sub1("minus(1)");
        TEST_EXPECT_APPLY_FAILURE(&sub1, env, "Expected single character result from ACI (but received 2 chars; while '.' -> '-1')");
    }

    // ------------------------------ "....664--2662440-44-61662664462-----4--4------662440...."
    TEST_EXPECT_APPLY_RESULT(op, env, "----553--155133--33-50551553351-----3--3------55133-----");

    TEST_OP_CFG_CONV_BIJECTIVE(op, "aci='minus(1)|head(1)'", env);

    // @@@ test multiple input strings
    // @@@ test input strings with varying lengths (how to handle?)
    // @@@ test empty input data -> error

    TEST_OPERATOR_PRODUCTION_FAILS(SOP_ACI, "", "missing 'aci' entry");

    GB_close(gb_main);
}

#endif // UNIT_TESTS

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