// ========================================================= //
//                                                           //
//   File      : xfergui.cxx                                 //
//   Purpose   : GUI to configure transfer sets              //
//                                                           //
//   Coded by Ralf Westram (coder@reallysoft.de) in Apr 19   //
//   http://www.arb-home.de/                                 //
//                                                           //
// ========================================================= //

#include "xfergui.h"
#include <xferset.h>

#include <prompt.hxx>

#include <aw_window.hxx>
#include <aw_root.hxx>
#include <aw_awar.hxx>
#include <aw_file.hxx>
#include <aw_msg.hxx>
#include <aw_select.hxx>

#include <arb_file.h>
#include <arb_str.h>
#include <StrUniquifier.h>
#include <FileWatch.h>

#include <set>
#include <string>

using namespace std;
using namespace FieldTransfer;

// ---------------
//      awars
#define AWARBASE_XFERSET_TMP  "tmp/fts/"
#define AWARBASE_XFERRULE_TMP AWARBASE_XFERSET_TMP "rule/"

#define AWAR_XFERSET_SELECTED  AWARBASE_XFERSET_TMP "focus" // Note: FTS clients like importer or exporter react if this awar gets touched
#define AWAR_XFERSET_COMMENT   AWARBASE_XFERSET_TMP "comment"
#define AWAR_XFERSET_UNDEFINED AWARBASE_XFERSET_TMP "undefined"

// fts-selection box (selects files, similar to ift/eft-selection-boxes)
#define AWAR_XFERSET_FTSBASE   AWARBASE_XFERSET_TMP "file"
#define AWAR_XFERSET_FTSNAME   AWAR_XFERSET_FTSBASE "/file_name"
// #define AWAR_XFERSET_FTSFILTER AWAR_XFERSET_FTSBASE "/filter"
// #define AWAR_XFERSET_FTSDIR    AWAR_XFERSET_FTSBASE "/directory"

#define AWAR_XFERRULE_SELECTED       AWARBASE_XFERRULE_TMP "focus"
#define AWAR_XFERRULE_TARGETFIELD    AWARBASE_XFERRULE_TMP "rule"
#define AWAR_XFERRULE_ACI            AWARBASE_XFERRULE_TMP "aci"
#define AWAR_XFERRULE_SEP            AWARBASE_XFERRULE_TMP "sep"
#define AWAR_XFERRULE_TYPE           AWARBASE_XFERRULE_TMP "type"
#define AWAR_XFERRULE_LOSS           AWARBASE_XFERRULE_TMP "loss"
#define AWAR_XFERRULE_INPUT_FIELDS   AWARBASE_XFERRULE_TMP "input/fields" // contains list of fieldnames separated by ';'
#define AWAR_XFERRULE_INPUT_SELECTED AWARBASE_XFERRULE_TMP "input/focus"  // field selected in AWAR_XFERRULE_INPUT_FIELDS (duplicate entries contain suffix! better not use content of this awar)
#define AWAR_XFERRULE_AVAIL_SELECTED AWARBASE_XFERRULE_TMP "avail"        // selected available field
#define AWAR_XFERRULE_AVAIL_CATEGORY AWARBASE_XFERRULE_TMP "acat"         // shown available fields
#define AWAR_XFERRULE_FIELD          AWARBASE_XFERRULE_TMP "field"        // content of fieldname textbox

// -------------------------
//      other constants
#define NO_XFERSET_SELECTED ""

// ---------------------
//      field order

inline bool is_info(const char *field) { return field[0] == '<'; }
inline bool is_info(const string& field) { return is_info(field.c_str()); }

struct lt_field { // set order type for field selection
    bool operator()(const string& s1, const string& s2) const {
        int cmp = is_info(s1)-is_info(s2);
        if (!cmp) cmp = s1.compare(s2);
        return cmp<0;
    }
};

// -----------------
//      globals

static const AvailableFieldScanner *currentFieldScanner = NULp;

typedef set<string, lt_field> StrSet;
typedef StrSet::iterator      StrSetIter;

static StrArray knownFieldsClientInput;     // known database fields (retrieved via AvailableFieldScanner; input fields)
static StrArray knownFieldsClientOutput;    // ---------- (dito, but output fields)
static StrArray knownFieldsRulesetInput;    // database fields read by current ruleset
static StrArray knownFieldsRulesetOutput;   // ---------- (dito, but fields written)
static StrSet   knownFields;                // known database fields (merged from misc. sources according to AWAR_XFERRULE_AVAIL_CATEGORY)

// --------------------------
//      helper functions

const char *XFER_getFullFTS(const char *name) {
/*! converts name (as contained in awar passed to XFER_select_RuleSet)
 * into full path.
     */
    static string result;
    if (name[0])  result = GB_concat_path(GB_path_in_arbprop("fts"), GBS_global_string("%s.fts", name));
    else          result = name;
    return result.c_str();
}

inline AW_awar *awar_selected_FTS() {
    return AW_root::SINGLETON->awar(AWAR_XFERSET_SELECTED);
}
static const char *get_selected_FTS() { // existing or new
    const char *newname = awar_selected_FTS()->read_char_pntr();
    return XFER_getFullFTS(newname);
}
inline void set_selected_FTS(const char *name) {
    awar_selected_FTS()->write_string(name);
}

inline char *getNameOnly(const char *fullpath) {
    char *nameOnly = NULp;
    GB_split_full_path(fullpath, NULp, NULp, &nameOnly, NULp);
    return nameOnly;
}

static RuleSetPtr getSelectedRuleset(const char*& failReason) {
    RuleSetPtr ruleset;
    string     fts = get_selected_FTS();
    failReason     = NULp;

    if (GB_is_readablefile(fts.c_str())) {
        ErrorOrRuleSetPtr loaded = RuleSet::loadFrom(fts.c_str());
        if (loaded.hasError()) {
            loaded.getError().set_handled();
            failReason = "load error";
        }
        else {
            ruleset = loaded.getValue();
        }
    }
    else {
        failReason = "no transfer set";
    }
    return ruleset;
}

inline int getSelectedRuleIndex() {
    /*! returns index of selected Rule [0..N-1]
     * or -1 if none selected.
     */
    int         idx     = -1;
    const char *rule_id = AW_root::SINGLETON->awar(AWAR_XFERRULE_SELECTED)->read_char_pntr();
    if (ARB_strBeginsWith(rule_id, "rule")) {
        idx = atoi(rule_id+4)-1;
        xf_assert(idx>=0);
    }
    return idx;
}
inline const char *ruleSelId(int idx) {
    return GBS_global_string("rule%i", idx+1);
}
inline GB_ERROR checkValidIndex(RuleSetPtr ruleset, int idx) {
    /*! return error if 'idx' invalid */
    return ruleset->validIdx(idx) ? NULp : "rule index out-of-bounds";
}

static void selectRule(int idx, int ruleCount) {
    /*! select rule in rule selection list
     * @param idx rule number inside RuleSet. allowed range: [0..N-1]. if idx is outside range -> deselect rule.
     * @param ruleCount amount of defined rules (N).
     */
    const char  *ruleId = idx>=0 && idx<ruleCount ? ruleSelId(idx) : "";
    AW_root::SINGLETON->awar(AWAR_XFERRULE_SELECTED)->write_string(ruleId);
}
static void deselectRule() { selectRule(0, 0); }

static RulePtr getSelectedRule(const char*& failReason) {
    RulePtr rule;
    failReason = NULp;

    int idx = getSelectedRuleIndex();
    if (idx<0) {
        failReason = "no rule";
    }
    else {
        RuleSetPtr ruleset = getSelectedRuleset(failReason);
        if (ruleset.isSet()) {
            failReason = checkValidIndex(ruleset, idx);
            if (!failReason) rule = ruleset->getPtr(idx);
        }
    }

    return rule;
}

inline void refresh_fts_selbox() {
    AW_refresh_fileselection(AW_root::SINGLETON, AWAR_XFERSET_FTSBASE);
}
inline GB_ERROR saveChangedRuleset(RuleSetPtr ruleset) {
    GB_ERROR error = NULp;
    string   fts   = get_selected_FTS();
    if (fts.empty()) {
        error = "no ruleset selected"; // (should not happen)
    }
    else {
        error = ruleset->saveTo(fts.c_str());
        if (!error) {
            refresh_fts_selbox();
            awar_selected_FTS()->touch();
        }
    }
    return error;
}

static int  onlyLintRuleWithIdx       = -1;   // -1 or idx if rule was updated in ruleset editor
static bool warnAboutDuplicateTargets = true;
static void lintRuleset(RuleSetPtr ruleset) {
    // examine ruleset (and rules within) and warn if problems are detected
    xf_assert(ruleset.isSet());

    typedef map<string, int> StrMap;

    bool   testsAllRules   = onlyLintRuleWithIdx < 0;
    int    warningsPrinted = 0;
    StrMap targetFieldCount;
    string checkDupOf;

    for (size_t r = 0; r<ruleset->size(); ++r) {
        const Rule&   rule = ruleset->get(r);
        const string& dest = rule.targetField();
        {
            StrMap::iterator found = targetFieldCount.find(dest);
            if (found == targetFieldCount.end()) {
                targetFieldCount[dest] = 1;
            }
            else {
                ++found->second;
            }
        }

        bool lintThisRule = testsAllRules || (onlyLintRuleWithIdx == int(r));
        if (lintThisRule) {
            if (dest == "name") { // @@@ this is a hack (later use itemtype when provided)
                aw_message("Warning: potentially dangerous target field 'name' detected");
                ++warningsPrinted;
            }
            if (targetFieldCount[dest] == 2) {
                if (warnAboutDuplicateTargets) {
                    aw_message(GBS_global_string("Warning: duplicated target field '%s' detected", dest.c_str()));
                    ++warningsPrinted;
                }
            }
            else if (!testsAllRules) {
                checkDupOf = dest;
            }
        }
    }

    if (!checkDupOf.empty()) {
        if (targetFieldCount[checkDupOf]>2) {
            if (warnAboutDuplicateTargets) {
                aw_message(GBS_global_string("Warning: duplicated target field '%s' detected", checkDupOf.c_str()));
                ++warningsPrinted;
            }
        }
    }

    if (warningsPrinted) {
        aw_message(GBS_global_string("In %s:", get_selected_FTS()));
    }
}

static void lintRulesetOnce(RuleSetPtr ruleset) {
    // remember filename and call lintRuleset only on change
    xf_assert(ruleset.isSet());

    string        fts    = get_selected_FTS();
    static string last_linted_fts;
    bool          lintIt = false;

    if (last_linted_fts.empty()) {
        lintIt = true;
    }
    else if (fts != last_linted_fts) {
        lintIt = true;
    }
    else if (onlyLintRuleWithIdx>=0) {
        lintIt = true;
    }

    if (lintIt) {
        lintRuleset(ruleset);
        last_linted_fts = fts;
    }
}

static void overwriteSelectedRule(RulePtr newRule) {
    GB_ERROR error = NULp;
    int      idx   = getSelectedRuleIndex();

    if (idx<0) {
        error = "no rule selected. update impossible.";
    }
    else {
        RuleSetPtr ruleset = getSelectedRuleset(error);
        if (ruleset.isSet()) {
            error = checkValidIndex(ruleset, idx);
            if (!error) {
                ruleset->replace(idx, newRule);
                LocallyModify<int> avoidWarningsAboutOtherRules(onlyLintRuleWithIdx, idx);
                error = saveChangedRuleset(ruleset);
            }
        }
    }
    aw_message_if(error);
}

inline GB_ERROR check_valid_existing_fts(const char *fullfts) {
    return GB_is_readablefile(fullfts) ? NULp : "no FTS selected";
}
static GB_ERROR check_valid_target_fts(const char *fullfts) {
    GB_ERROR error = NULp;
    if (!fullfts[0]) {
        error = "no name specified";
    }
    else if (GB_is_readablefile(fullfts)) {
        char *fts_nameOnly = getNameOnly(fullfts);
        error = GBS_global_string("field transfer set '%s' already exists",
                                  fts_nameOnly ? fts_nameOnly : fullfts);
        free(fts_nameOnly);
    }
    return error;
}

// --------------------------------
//      Rule definition window

static bool ignoreRuleDetailChange = false;

static void selected_rule_changed_cb(AW_root *awr) {
    const char *failReason;
    RulePtr     rule = getSelectedRule(failReason);

    LocallyModify<bool> duringUpdate(ignoreRuleDetailChange, true);
    const bool          haveRule = rule.isSet();

    awr->awar(AWAR_XFERRULE_TARGETFIELD)->write_string(haveRule ? rule->targetField().c_str() : "");
    awr->awar(AWAR_XFERRULE_ACI)->write_string(haveRule ? rule->getACI().c_str() : "");
    awr->awar(AWAR_XFERRULE_SEP)->write_string(haveRule ? rule->getSeparator().c_str() : "");

    GB_TYPES forcedType = GB_NONE;
    if (haveRule && rule->forcesType()) forcedType = rule->getTargetType();
    awr->awar(AWAR_XFERRULE_TYPE)->write_int(forcedType);
    awr->awar(AWAR_XFERRULE_LOSS)->write_int(haveRule && rule->precisionLossPermitted());
    awr->awar(AWAR_XFERRULE_INPUT_FIELDS)->write_string(haveRule ? rule->getSourceFields().c_str() : "");
}

static RulePtr build_rule_from_awars(AW_root *awr) {
    // create a rule from settings currently selected in definition window
    RulePtr newRule;

    const char *src  = awr->awar(AWAR_XFERRULE_INPUT_FIELDS)->read_char_pntr();
    const char *dest = awr->awar(AWAR_XFERRULE_TARGETFIELD)->read_char_pntr();
    const char *aci  = awr->awar(AWAR_XFERRULE_ACI)->read_char_pntr();
    const char *sep  = awr->awar(AWAR_XFERRULE_SEP)->read_char_pntr();

    newRule = aci[0] ? Rule::makeAciConverter(src, sep, aci, dest) : Rule::makeSimple(src, sep, dest);

    GB_TYPES type       = GB_TYPES(awr->awar(AWAR_XFERRULE_TYPE)->read_int());
    bool     permitLoss = awr->awar(AWAR_XFERRULE_LOSS)->read_int();

    if (type != GB_NONE) newRule->setTargetType(type);
    if (permitLoss)      newRule->permitPrecisionLoss();

    return newRule;
}

static void rebuild_rule_from_awars_cb(AW_root *awr) {
    // rebuild a Rule from current AWAR settings
    // (called back whenever any awar changes)
    if (!ignoreRuleDetailChange) {
        LocallyModify<bool> duringRebuild(ignoreRuleDetailChange, true);

        const char *failReason;
        RulePtr     selRule = getSelectedRule(failReason);
        RulePtr     newRule = build_rule_from_awars(awr);

#if defined(DEBUG)
        printf("rebuild_rule_from_awars_cb:\n");
        // dump both configs (of rebuild and of stored rule) allowing to compare them:
        if (selRule.isSet()) printf("  selRule config: %s\n", selRule->getConfig().c_str());
        if (newRule.isSet()) printf("  newRule config: %s\n", newRule->getConfig().c_str());
#endif

        if (selRule.isSet() && newRule.isSet()) {
            string selCfg = selRule->getConfig();
            string newCfg = newRule->getConfig();
            if (selCfg != newCfg) { // has rule been changed?
#if defined(DEBUG)
                fputs("Rule has changed!\n", stdout);
#endif
                overwriteSelectedRule(newRule);
            }
        }
    }
}

static void availfield_selected_cb(AW_root *awr) {
    char *selField = awr->awar(AWAR_XFERRULE_AVAIL_SELECTED)->read_string();
    awr->awar(AWAR_XFERRULE_FIELD)->write_string(selField);
    free(selField);
}

static void refresh_availfield_selbox_cb(AW_root *awr, AW_selection_list *avail) {
    // called if (a) list of fields changed or
    //           (b) extraction substring changed

    const char *part = awr->awar(AWAR_XFERRULE_FIELD)->read_char_pntr();

    // detect whether content of AWAR_XFERRULE_FIELD is partial or full field name:
    bool isPart = part[0];
    for (StrSetIter f = knownFields.begin(); f != knownFields.end() && isPart; ++f) {
        isPart = *f != part; // stop when field matches part
    }

    avail->clear();

    int fieldCount      = 0;
    int shownFieldCount = 0;
    for (StrSetIter f = knownFields.begin(); f != knownFields.end(); ++f) {
        const char *field = f->c_str();
        if (is_info(field)) {
            avail->insert(field, "");
        }
        else {
            ++fieldCount;
            if (!isPart || strcasestr(field, part)) { // filter displayed entries based on AWAR_XFERRULE_FIELD content (if isPart)
                avail->insert(field, field);
                ++shownFieldCount;
            }
        }
    }

    // add default (showing info):
    {
        const char *info = "";

        if (fieldCount == 0) info = "<no fields detected>";
        else if (isPart) {
            if (shownFieldCount>0) info = GBS_global_string("<fields matching '%s'>", part);
            else                   info = GBS_global_string("<no field matches '%s'>", part);
        }

        avail->insert_default(info, "");
    }
    avail->update();

    awr->awar(AWAR_XFERRULE_AVAIL_SELECTED)->write_string(part); // select name (or default) in list
}

enum AvailCategory {
    ALL_AVAILABLE_FIELDS,
    FIELDS_BY_RULESET,
    UNREAD_BY_RULESET,
    UNWRITTEN_BY_RULESET,
    INPUT_FIELDS_BY_CLIENT,
    OUTPUT_FIELDS_BY_CLIENT,
};

inline void mergeToKnownFields(const StrArray& source) {
    for (unsigned i = 0; i<source.size(); ++i) {
        knownFields.insert(source[i]);
    }
}
inline size_t removeFromKnownFields(const StrArray& unwanted) {
    size_t removed = 0;
    for (unsigned i = 0; i<unwanted.size(); ++i) {
        removed += knownFields.erase(unwanted[i]);
    }
    return removed;
}

static void mergeKnownFields(AW_root *awr) {
    // merge fields from client and from RuleSet
    AvailCategory cat = AvailCategory(awr->awar(AWAR_XFERRULE_AVAIL_CATEGORY)->read_int());

    knownFields.clear();
    switch (cat) {
        case INPUT_FIELDS_BY_CLIENT:  mergeToKnownFields(knownFieldsClientInput);  break;
        case OUTPUT_FIELDS_BY_CLIENT: mergeToKnownFields(knownFieldsClientOutput); break;

        case FIELDS_BY_RULESET:
            mergeToKnownFields(knownFieldsRulesetInput);
            mergeToKnownFields(knownFieldsRulesetOutput);
            break;

        case UNREAD_BY_RULESET: {
            mergeToKnownFields(knownFieldsClientInput);
            size_t removed = removeFromKnownFields(knownFieldsRulesetInput);
            knownFields.insert(GBS_global_string("<read fields: %zu>", removed));
            break;
        }

        case UNWRITTEN_BY_RULESET: {
            mergeToKnownFields(knownFieldsClientOutput);
            size_t removed = removeFromKnownFields(knownFieldsRulesetOutput);
            knownFields.insert(GBS_global_string("<written fields: %zu>", removed));
            break;
        }

        case ALL_AVAILABLE_FIELDS:
            mergeToKnownFields(knownFieldsClientInput);
            mergeToKnownFields(knownFieldsClientOutput);
            mergeToKnownFields(knownFieldsRulesetInput);
            mergeToKnownFields(knownFieldsRulesetOutput);
            break;
    }


    awr->awar(AWAR_XFERRULE_FIELD)->touch(); // calls refresh_availfield_selbox_cb
}

static void refresh_available_fields_from_ruleset(AW_root *awr, RuleSetPtr rulesetPtr) {
    knownFieldsRulesetInput.erase();
    knownFieldsRulesetOutput.erase();
    if (rulesetPtr.isSet()) {
        rulesetPtr->extractUsedFields(knownFieldsRulesetInput, knownFieldsRulesetOutput);
    }
    else {
        knownFieldsRulesetOutput.put(strdup("<no FTS selected>"));
    }
    mergeKnownFields(awr);
}
void XFER_refresh_available_fields(AW_root *awr, const AvailableFieldScanner *fieldScanner, FieldsToScan whatToScan) {
    /*! refreshes the available fields (defined by client) shown in fts gui,
     * if 'fieldScanner' is the currently active scanner.
     * Otherwise do nothing (because GUI "belongs" to different client).
     */
    if (fieldScanner == currentFieldScanner) {
        if (whatToScan & SCAN_INPUT_FIELDS) {
            knownFieldsClientInput.erase();
            currentFieldScanner->scanFields(knownFieldsClientInput, SCAN_INPUT_FIELDS);
        }
        if (whatToScan & SCAN_OUTPUT_FIELDS) {
            knownFieldsClientOutput.erase();
            currentFieldScanner->scanFields(knownFieldsClientOutput, SCAN_OUTPUT_FIELDS);
        }
        mergeKnownFields(awr);
    }
}

static void refresh_inputfield_selbox_cb(AW_root *awr, AW_selection_list *input) {
    ConstStrArray field;
    {
        char *fieldsStr = awr->awar(AWAR_XFERRULE_INPUT_FIELDS)->read_string();
        GBT_splitNdestroy_string(field, fieldsStr, ";", SPLIT_DROPEMPTY);
    }

    // fill into selection box:
    input->clear();
    {
        StrUniquifier keyTrack; // avoid problems in list-handling caused by using multiple identical keys
        for (int i = 0; field[i]; ++i) {
            const char *disp = field[i];
            xf_assert(disp && disp[0]); // NULp / empty field unwanted

            const char *val = keyTrack.make_unique_key(field[i]);
            xf_assert(val);

            input->insert(disp, val);
        }
    }
    input->insert_default("", "");
    input->update();

    rebuild_rule_from_awars_cb(awr); // @@@ useful?
}

static void refresh_rule_selection_box_cb(AW_root *awr, AW_selection_list *rules) {
    const char *defaultText; // displays failures (to load RuleSet) in default entry
    RuleSetPtr  ruleset = getSelectedRuleset(defaultText);

    rules->clear();
    if (!defaultText) {
        xf_assert(ruleset.isSet());

        lintRulesetOnce(ruleset);

        for (size_t r = 0; r<ruleset->size(); ++r) {
            const Rule& rule = ruleset->get(r);
            rules->insert(rule.getShortDescription().c_str(), ruleSelId(r));
        }
        defaultText = "no rule";
    }
    rules->insert_default(GBS_global_string("<%s>", defaultText), "");
    rules->update();

    selected_rule_changed_cb(awr);
    refresh_available_fields_from_ruleset(awr, ruleset);
}

static void init_rule_definition_awars(AW_root *awr) {
    awr->awar_string(AWAR_XFERRULE_SELECTED,       "", AW_ROOT_DEFAULT);
    awr->awar_string(AWAR_XFERRULE_TARGETFIELD,    "", AW_ROOT_DEFAULT);
    awr->awar_string(AWAR_XFERRULE_ACI,            "", AW_ROOT_DEFAULT);
    awr->awar_string(AWAR_XFERRULE_SEP,            "", AW_ROOT_DEFAULT);
    awr->awar_string(AWAR_XFERRULE_INPUT_SELECTED, "", AW_ROOT_DEFAULT);
    awr->awar_string(AWAR_XFERRULE_INPUT_FIELDS,   "", AW_ROOT_DEFAULT);
    awr->awar_string(AWAR_XFERRULE_AVAIL_SELECTED, "", AW_ROOT_DEFAULT);
    awr->awar_string(AWAR_XFERRULE_FIELD,          "", AW_ROOT_DEFAULT);

    awr->awar_int(AWAR_XFERRULE_AVAIL_CATEGORY, ALL_AVAILABLE_FIELDS, AW_ROOT_DEFAULT);
    awr->awar_int(AWAR_XFERRULE_TYPE,           GB_NONE,              AW_ROOT_DEFAULT);
    awr->awar_int(AWAR_XFERRULE_LOSS,           0,                    AW_ROOT_DEFAULT);
}

static void clear_field_cb(AW_window *aww) {
    aww->get_root()->awar(AWAR_XFERRULE_FIELD)->write_string("");
}

static void add_rule_cb(AW_window *aww) {
    GB_ERROR error = NULp;

    RuleSetPtr ruleset = getSelectedRuleset(error);
    if (ruleset.isSet()) {
        int     idx = getSelectedRuleIndex();
        RulePtr toAdd;

        if (idx == -1) {
            // if no rule is selected -> settings may be changed w/o changing any rule.
            // Clicking ADD in that situation builds and adds a new rule from these settings:
            toAdd = build_rule_from_awars(aww->get_root());
        }
        else {
            toAdd = Rule::makeSimple("", "", ""); // create empty default rule
        }
        {
            int newIdx = ruleset->insertBefore(idx, toAdd);
            LocallyModify<int> avoidWarningsAboutOtherRules(onlyLintRuleWithIdx, newIdx);
            error = saveChangedRuleset(ruleset);
        }
        if (!error) {
            deselectRule();
            clear_field_cb(aww);
        }
    }
    aw_message_if(error);
}
static void del_rule_cb(AW_window*) {
    GB_ERROR error = NULp;
    int      idx   = getSelectedRuleIndex();

    if (idx<0) {
        error = "no rule selected. nothing deleted.";
    }
    else {
        RuleSetPtr ruleset = getSelectedRuleset(error);
        if (ruleset.isSet()) {
            error = checkValidIndex(ruleset, idx);
            if (!error) {
                ruleset->erase(idx);
                error = saveChangedRuleset(ruleset);
                if (!error) selectRule(idx, ruleset->size());
            }
        }
    }
    aw_message_if(error);
}
static void rule_stack_cb(AW_window*, bool toStack) {
    static RuleContainer stack; // uses one stack for all Rulesets

    GB_ERROR   error     = NULp;
    const bool fromStack = !toStack;

    if (fromStack && stack.empty()) {
        error = "nothing copied. cannot paste.";
    }
    else {
        int idx = getSelectedRuleIndex();

        if (idx<0 && toStack) {
            error = "no rule selected. nothing copied.";
        }
        else {
            RuleSetPtr ruleset = getSelectedRuleset(error);
            if (ruleset.isSet()) {
                if (toStack) {
                    error = checkValidIndex(ruleset, idx);
                    if (!error) {
                        stack.push_back(ruleset->getPtr(idx));
                        selectRule(idx+1, ruleset->size());
                    }
                }
                else { // fromStack
                    xf_assert(!stack.empty());
                    RulePtr rule   = stack.back();
                    int     newIdx = ruleset->insertBefore(idx, rule);
                    {
                        LocallyModify<int>  avoidWarningsAboutOtherRules(onlyLintRuleWithIdx, newIdx);
                        LocallyModify<bool> avoidDupWarningsWhenPasting(warnAboutDuplicateTargets, false);
                        error = saveChangedRuleset(ruleset);
                    }
                    if (!error) {
                        selectRule(newIdx, ruleset->size());
                        stack.pop_back();
                    }
                }
            }
            else if (fromStack) {
                error = "did not paste to nowhere (no ruleset selected)";
            }
        }
    }
    aw_message_if(error);
}

inline void updateChangedInputFields(const ConstStrArray& ifield, AW_awar *awar_input_fields) {
    char *new_input_fields = GBT_join_strings(ifield, ';');
    awar_input_fields->write_string(new_input_fields);
    free(new_input_fields);
}

static void add_field_cb(AW_window *aww, AW_selection_list *inputSel) {
    AW_root    *awr   = aww->get_root();
    const char *field = awr->awar(AWAR_XFERRULE_FIELD)->read_char_pntr();

    if (!field[0]) { // empty fieldname
        aw_message("Please select an available field or\n"
                   "enter a custom field name in the textbox below.");
    }
    else {
        GB_ERROR keyError = GB_check_hkey(field);
        if (keyError) {
            aw_message(keyError);
        }
        else if (inputSel) {
            AW_awar    *awar_input_fields = awr->awar(AWAR_XFERRULE_INPUT_FIELDS);
            const char *used_input_fields = awar_input_fields->read_char_pntr();

            int selIdx;
            if (used_input_fields[0]) { // if we already have fields..
                ConstStrArray ifield;
                GBT_split_string(ifield, used_input_fields, ";", SPLIT_DROPEMPTY);
                selIdx = inputSel->get_index_of_selected();
                ifield.put_before(selIdx, field);
                updateChangedInputFields(ifield, awar_input_fields);
            }
            else { // insert 1st field
                awar_input_fields->write_string(field);
                selIdx = 0;
            }
            inputSel->select_element_at(selIdx);
        }
        else {
            awr->awar(AWAR_XFERRULE_TARGETFIELD)->write_string(field);
        }
    }
}

enum FieldMoveDest { MV_UP, MV_DOWN, MV_OFF };
static void move_field_cb(AW_window *aww, FieldMoveDest dest, AW_selection_list *inputSel) {
    AW_root    *awr               = aww->get_root();
    AW_awar    *awar_input_fields = awr->awar(AWAR_XFERRULE_INPUT_FIELDS);
    const char *used_input_fields = awar_input_fields->read_char_pntr();

    if (used_input_fields[0]) {
        int selIdx = inputSel->get_index_of_selected();
        if (selIdx>=0) { // is any field selected?
            ConstStrArray ifield;
            GBT_split_string(ifield, used_input_fields, ";", SPLIT_DROPEMPTY);

            int  maxIdx     = ifield.size()-1;
            bool needUpdate = false;

            switch (dest) {
                case MV_OFF:
                    ifield.remove(selIdx);
                    needUpdate = true;
                    break;
                case MV_DOWN:
                    if (selIdx<maxIdx) {
                        ifield.move(selIdx, selIdx+1);
                        selIdx++;
                        needUpdate = true;
                    }
                    break;
                case MV_UP:
                    if (selIdx>0) {
                        ifield.move(selIdx, selIdx-1);
                        selIdx--;
                        needUpdate = true;
                    }
                    break;
            }

            if (needUpdate) {
                updateChangedInputFields(ifield, awar_input_fields);
                inputSel->select_element_at(selIdx);
            }
        }
    }
}

static void popup_rule_definition_window(AW_root *awr) {
    static AW_window *aw_define = NULp;
    if (!aw_define) {
        AW_window_simple *aws = new AW_window_simple;
        aws->init(awr, "DEFINE_RULE", "Define rules for field transfer");
        aws->load_xfig("fts_ruledef.fig");

        aws->button_length(8);

        aws->at("close");
        aws->callback(AW_POPDOWN);
        aws->create_button("CLOSE", "CLOSE", "C");

        aws->at("help");
        aws->callback(makeHelpCallback("xferset.hlp"));
        aws->create_button("HELP", "HELP", "H");

        // selection lists
        aws->at("rules");
        AW_selection_list *rules = aws->create_selection_list(AWAR_XFERRULE_SELECTED);

        aws->at("input");
        AW_selection_list *input = aws->create_selection_list(AWAR_XFERRULE_INPUT_SELECTED);

        aws->at("avail");
        AW_selection_list *avail = aws->create_selection_list(AWAR_XFERRULE_AVAIL_SELECTED);

        // section below left selection list:
        aws->at("undef");
        aws->label("Transfer undefined fields?");
        aws->create_toggle(AWAR_XFERSET_UNDEFINED);

        // section below middle and right selection lists:
        aws->at("acat");
        aws->create_option_menu(AWAR_XFERRULE_AVAIL_CATEGORY);
        aws->insert_default_option("all",       "a", ALL_AVAILABLE_FIELDS);
        aws->insert_option        ("input",     "i", INPUT_FIELDS_BY_CLIENT);
        aws->insert_option        ("output",    "o", OUTPUT_FIELDS_BY_CLIENT);
        aws->insert_option        ("ruleset",   "s", FIELDS_BY_RULESET);
        aws->insert_option        ("unread",    "r", UNREAD_BY_RULESET);
        aws->insert_option        ("unwritten", "w", UNWRITTEN_BY_RULESET);
        aws->update_option_menu();

        aws->at("field");
        aws->create_input_field(AWAR_XFERRULE_FIELD);

        aws->at("sep");    aws->create_input_field(AWAR_XFERRULE_SEP);
        aws->at("aci");    aws->create_input_field(AWAR_XFERRULE_ACI);
        aws->at("target"); aws->create_input_field(AWAR_XFERRULE_TARGETFIELD);

        aws->at("type");
        aws->create_option_menu(AWAR_XFERRULE_TYPE);
        aws->insert_option        ("Ascii text",        "s", GB_STRING);
        aws->insert_option        ("Rounded numerical", "i", GB_INT);
        aws->insert_option        ("Floating-point n.", "F", GB_FLOAT);
        aws->insert_option        ("Bitmask (0/1)",     "B", GB_BITS);
        aws->insert_default_option("Automatic",         "A", GB_NONE);
        aws->update_option_menu();

        aws->at("loss"); aws->create_toggle(AWAR_XFERRULE_LOSS);

        // ---------------------------------------
        //      buttons below selection lists
        aws->button_length(6);
        aws->auto_space(5, 5);

        aws->at("rbut"); // left sellist
        aws->callback(add_rule_cb);                              aws->create_button("ADD_RULE",   "ADD");
        aws->callback(del_rule_cb);                              aws->create_button("DEL_RULE",   "DEL");
        aws->callback(makeWindowCallback(rule_stack_cb, true));  aws->create_button("COPY_RULE",  "COPY");
        aws->callback(makeWindowCallback(rule_stack_cb, false)); aws->create_button("PASTE_RULE", "PASTE");

        aws->at("idel"); // center sellist
        aws->callback(makeWindowCallback(move_field_cb, MV_OFF, input));  aws->create_button("INPUT_DROP", "DROP");
        aws->callback(makeWindowCallback(move_field_cb, MV_UP, input));   aws->create_button("INPUT_UP",   "UP");
        aws->callback(makeWindowCallback(move_field_cb, MV_DOWN, input)); aws->create_button("INPUT_DOWN", "DOWN");

        aws->at("iadd"); // right sellist
        aws->callback(makeWindowCallback(add_field_cb, input));                    aws->create_button("INPUT_ADD",  "FROM");
        aws->callback(makeWindowCallback(add_field_cb, (AW_selection_list*)NULp)); aws->create_button("OUTPUT_ADD", "TO");
        aws->callback(makeWindowCallback(clear_field_cb));                         aws->create_button("CLEAR",      "CLEAR");

        // bind awar callbacks:
        awr->awar(AWAR_XFERSET_SELECTED)       ->add_callback(makeRootCallback(refresh_rule_selection_box_cb, rules));
        awr->awar(AWAR_XFERRULE_SELECTED)      ->add_callback(selected_rule_changed_cb);
        awr->awar(AWAR_XFERRULE_INPUT_FIELDS)  ->add_callback(makeRootCallback(refresh_inputfield_selbox_cb, input));
        awr->awar(AWAR_XFERRULE_FIELD)         ->add_callback(makeRootCallback(refresh_availfield_selbox_cb, avail));
        awr->awar(AWAR_XFERRULE_AVAIL_CATEGORY)->add_callback(mergeKnownFields);
        awr->awar(AWAR_XFERRULE_AVAIL_SELECTED)->add_callback(availfield_selected_cb);

        awr->awar(AWAR_XFERRULE_TARGETFIELD)->add_callback(rebuild_rule_from_awars_cb);
        awr->awar(AWAR_XFERRULE_ACI)        ->add_callback(rebuild_rule_from_awars_cb);
        awr->awar(AWAR_XFERRULE_SEP)        ->add_callback(rebuild_rule_from_awars_cb);
        awr->awar(AWAR_XFERRULE_TYPE)       ->add_callback(rebuild_rule_from_awars_cb);
        awr->awar(AWAR_XFERRULE_LOSS)       ->add_callback(rebuild_rule_from_awars_cb);

        awar_selected_FTS()->touch();              // calls refresh_rule_selection_box_cb
        awr->awar(AWAR_XFERRULE_FIELD)->touch();   // calls refresh_availfield_selbox_cb

        aw_define = aws;
    }
    aw_define->activate();
}

// ----------------------------------------------
//      AWAR change callbacks (RuleSetAdmin)

static void fts_filesel_changed_cb(AW_root *awr) {
    static bool recursion = false;
    if (!recursion) {
        LocallyModify<bool> avoid(recursion, true);

        char *fts          = AW_get_selected_fullname(awr, AWAR_XFERSET_FTSBASE);
        char *fts_nameOnly = getNameOnly(fts);

        if (fts_nameOnly && !GB_is_directory(fts)) { // avoid directory (otherwise would write 'fts' to AWAR_XFERSET_SELECTED)
            set_selected_FTS(fts_nameOnly);
            free(fts_nameOnly);
        }
        else {
            set_selected_FTS(NO_XFERSET_SELECTED);
        }
        free(fts);
    }
}

static bool ignoreRulesetAwarChange = false;

static void selected_fts_changed_cb(AW_root *awr) {
    static bool recursion = false;
    if (!recursion) {
        LocallyModify<bool> avoid(recursion, true);

        string fts = get_selected_FTS();
        AW_set_selected_fullname(awr, AWAR_XFERSET_FTSBASE, fts.c_str());

        string comment;
        bool   transferUndef = false;
        if (GB_is_readablefile(fts.c_str())) {
            ErrorOrRuleSetPtr loaded = RuleSet::loadFrom(fts.c_str());
            if (loaded.hasError()) {
                aw_message_if(loaded.getError().deliver());
            }
            else {
                const RuleSet& ruleset = *loaded.getValue();

                comment       = ruleset.getComment();
                transferUndef = ruleset.shallTransferUndefFields();
            }
        }
        LocallyModify<bool> duringCommentReload(ignoreRulesetAwarChange, true);
        awr->awar(AWAR_XFERSET_COMMENT)->write_string(comment.c_str());
        awr->awar(AWAR_XFERSET_UNDEFINED)->write_int(transferUndef);
    }
}

static void ruleset_awar_changed_cb(AW_root *awr, const char *awarname) {
    if (!ignoreRulesetAwarChange) {
        string fts = get_selected_FTS();
        if (GB_is_readablefile(fts.c_str())) {
            ErrorOrRuleSetPtr loaded = RuleSet::loadFrom(fts.c_str());
            if (loaded.hasError()) {
                aw_message_if(loaded.getError().deliver());
            }
            else {
                RuleSetPtr ruleset = loaded.getValue();
                bool       save    = false;

                if (strcmp(awarname, AWAR_XFERSET_COMMENT) == 0) {
                    string      saved   = ruleset->getComment();
                    const char *changed = awr->awar(AWAR_XFERSET_COMMENT)->read_char_pntr();
                    if (strcmp(saved.c_str(), changed) != 0) { // comment was changed
                        ruleset->setComment(changed);
                        save = true;
                    }
                }
                else if (strcmp(awarname, AWAR_XFERSET_UNDEFINED) == 0) {
                    bool transferUndef = awr->awar(AWAR_XFERSET_UNDEFINED)->read_int();
                    if (transferUndef != ruleset->shallTransferUndefFields()) { // setting was changed
                        ruleset->set_transferUndefFields(transferUndef);
                        save = true;
                    }
                }
                else { xf_assert(0); } // unknown awar

                if (save) {
                    GB_ERROR error = ruleset->saveTo(fts.c_str());
                    aw_message_if(error);
                    refresh_fts_selbox();

                    awar_selected_FTS()->touch(); // refresh fts if setting is changed
                }
            }
        }
        else {
            aw_message("Please select an existing FTS, then try again to change this setting.");
        }
    }
}

// -----------------------------------------
//      button callbacks (RuleSetAdmin)
static void editRuleset_cb(AW_window *aww) {
    string   fullfts = get_selected_FTS();
    GB_ERROR error   = check_valid_existing_fts(fullfts.c_str());
    if (!error) popup_rule_definition_window(aww->get_root());
    aw_message_if(error);
}
static void createRuleset_cb(AW_window *aww) {
    string fullfts = get_selected_FTS();
    GB_ERROR error = check_valid_target_fts(fullfts.c_str());
    if (!error) {
        RuleSet empty;
        error = empty.saveTo(fullfts.c_str());
        if (!error) {
            refresh_fts_selbox();
            awar_selected_FTS()->touch(); // will trigger refresh on client-side (e.g. rerun test-import or -export)
            editRuleset_cb(aww);          // simulate press EDIT
        }
    }
    aw_message_if(error);
}

enum copyMoveMode {COPY_RULESET, MOVE_RULESET};
static GB_ERROR copyMoveRuleset_cb(const char *new_name, copyMoveMode mode) {
    // copy or rename the currently selected ruleset

    // error if no RuleSet selected
    string fullSourceFts = get_selected_FTS();
    GB_ERROR error = check_valid_existing_fts(fullSourceFts.c_str()); // (e.g. click COPY with selected, then deselect, then start copy)

    // build full target file name. fail if new_name exists.
    string fullTargetFts;
    if (!error) {
        char *path;
        GB_split_full_path(fullSourceFts.c_str(), &path, NULp, NULp, NULp);
        if (new_name[0]) {
            fullTargetFts = GB_concat_path(path, GB_append_suffix(new_name, "fts"));
        }
        error = check_valid_target_fts(fullTargetFts.c_str());
    }

    // copy or move file
    if (!error) {
        switch (mode) {
            case COPY_RULESET: error = GB_safe_copy_file(fullSourceFts.c_str(), fullTargetFts.c_str()); break;
            case MOVE_RULESET: error = GB_safe_rename_file(fullSourceFts.c_str(), fullTargetFts.c_str()); break;
        }
    }

    // force selbox refresh + select new
    if (!error) {
        set_selected_FTS(NO_XFERSET_SELECTED);
        refresh_fts_selbox();
        set_selected_FTS(new_name);
    }


    return error;
}

static void askCopyMoveRuleset_cb(AW_window *, copyMoveMode mode) {
    string   fullfts = get_selected_FTS();
    GB_ERROR error   = check_valid_existing_fts(fullfts.c_str());
    if (error) {
        aw_message(error);
    }
    else {
        const char *title  = NULp;
        const char *prompt = NULp;
        const char *butTxt = NULp;
        switch (mode) {
            case COPY_RULESET:
                title  = "Copy ruleset";
                prompt = "Enter the name of the new ruleset:";
                butTxt = "Copy";
                break;
            case MOVE_RULESET:
                title  = "Rename ruleset";
                prompt = "Enter the new name for the ruleset:";
                butTxt = "Rename";
                break;
        }

        char *old_name;
        GB_split_full_path(fullfts.c_str(), NULp, NULp, &old_name, NULp);
        AWT_activate_prompt(title, prompt, old_name, butTxt, makeResultHandler(copyMoveRuleset_cb, mode));
        free(old_name);
    }
}
static void deleteRuleset_cb() {
    string   fullfts = get_selected_FTS();
    GB_ERROR error   = check_valid_existing_fts(fullfts.c_str());
    if (!error) {
        if (GB_unlink(fullfts.c_str())<0) {
            error = GB_await_error();
        }
        else {
            set_selected_FTS(NO_XFERSET_SELECTED); // deselect
            refresh_fts_selbox();
        }
    }
    aw_message_if(error);
}

static void noRuleset_cb() {
    set_selected_FTS(NO_XFERSET_SELECTED); // deselect
    refresh_fts_selbox();
}

// ------------------------------
//      RuleSet admin window
static void selected_fts_file_changed(const char *, ChangeReason reason) {
    switch (reason) {
        case CR_MODIFIED:
            awar_selected_FTS()->touch(); // triggers reload of RuleSet
            break;

        case CR_DELETED:
            noRuleset_cb(); // = press NONE button
            break;

        case CR_CREATED: break;
    }
}

static void initXferAwars(AW_root *awr) {
    static bool initialized = false;
    if (!initialized) {
        awr->awar_string(AWAR_XFERSET_SELECTED,  "", AW_ROOT_DEFAULT);
        awr->awar_string(AWAR_XFERSET_COMMENT,   "", AW_ROOT_DEFAULT);
        awr->awar_int   (AWAR_XFERSET_UNDEFINED, 0,  AW_ROOT_DEFAULT);

        AW_create_fileselection_awars(awr, AWAR_XFERSET_FTSBASE, GB_path_in_arbprop("fts"), ".fts", "");

        awr->awar(AWAR_XFERSET_FTSNAME) ->add_callback(fts_filesel_changed_cb);
        awr->awar(AWAR_XFERSET_SELECTED)->add_callback(selected_fts_changed_cb);

        awr->awar(AWAR_XFERSET_COMMENT)  ->add_callback(makeRootCallback(ruleset_awar_changed_cb, AWAR_XFERSET_COMMENT));
        awr->awar(AWAR_XFERSET_UNDEFINED)->add_callback(makeRootCallback(ruleset_awar_changed_cb, AWAR_XFERSET_UNDEFINED));

        init_rule_definition_awars(awr);

        static FileWatch watchSelectedFts(AWAR_XFERSET_FTSNAME, makeFileChangedCallback(selected_fts_file_changed));

        initialized = true;
    }
}

static void popup_ruleset_admin_window(AW_root *awr) {
    static AW_window *aw_select = NULp;
    if (!aw_select) {
        AW_window_simple *aws = new AW_window_simple;
        aws->init(awr, "SELECT_FTS", "Select field transfer set (FTS)");
        aws->load_xfig("fts_select.fig");

        aws->button_length(8);

        aws->at("close");
        aws->callback(AW_POPDOWN);
        aws->create_button("CLOSE", "CLOSE", "C");

        aws->at("help");
        aws->callback(makeHelpCallback("xferset.hlp"));
        aws->create_button("HELP", "HELP", "H");

        aws->at("name");
        aws->create_input_field(AWAR_XFERSET_SELECTED);

        aws->at("comment");
        aws->create_text_field(AWAR_XFERSET_COMMENT);

        AW_create_fileselection(aws, AWAR_XFERSET_FTSBASE, "fts", ".", MULTI_DIRS, false);

        aws->at("create");
        aws->callback(makeWindowCallback(createRuleset_cb));
        aws->create_button("CREATE", "CREATE", "N");

        aws->at("edit");
        aws->callback(makeWindowCallback(editRuleset_cb));
        aws->create_button("EDIT", "EDIT", "E");

        aws->at("copy");
        aws->callback(makeWindowCallback(askCopyMoveRuleset_cb, COPY_RULESET));
        aws->create_button("COPY", "COPY", "Y");

        aws->at("delete");
        aws->callback(makeWindowCallback(deleteRuleset_cb));
        aws->create_button("DELETE", "DELETE", "D");

        aws->at("rename");
        aws->callback(makeWindowCallback(askCopyMoveRuleset_cb, MOVE_RULESET));
        aws->create_button("RENAME", "RENAME", "R");

        aws->at("none");
        aws->callback(makeWindowCallback(noRuleset_cb));
        aws->create_button("NONE", "NONE", "O");

        aw_select = aws;
    }
    aw_select->activate();
}

void XFER_select_RuleSet(AW_window *aww, const char *awar_selected_fts, const AvailableFieldScanner *fieldScanner) {
    /*! initializes ruleset GUI to choose/edit 'awar_selected_fts'.
     * May be bound via multiple callbacks; will always edit the awar of the last triggered callback.
     */

    AW_root *awr = aww->get_root();
    initXferAwars(awr);
    awr->awar(AWAR_XFERSET_SELECTED)->map(awar_selected_fts);

    if (fieldScanner != currentFieldScanner) {
        currentFieldScanner = fieldScanner;
        XFER_refresh_available_fields(awr, currentFieldScanner, SCAN_ALL_FIELDS);
    }

    popup_ruleset_admin_window(awr);
}

