// ================================================================ //
//                                                                  //
//   File      : nds.cxx                                            //
//   Purpose   : Node Display Setup                                 //
//                                                                  //
//   Institute of Microbiology (Technical University Munich)        //
//   http://www.arb-home.de/                                        //
//                                                                  //
// ================================================================ //

#include "nds.h"
#include <config_manager.hxx>
#include <sel_boxes.hxx>

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

#include <TreeNode.h>
#include <items.h>
#include <item_sel_list.h>
#include <gb_aci.h>

#include <arb_msg_fwd.h>
#include <arb_global_defs.h>
#include <arb_strbuf.h>

#define nds_assert(cond) arb_assert(cond)

#define NDS_PER_PAGE 10         // number of NDS definitions on each config-page
#define NDS_PAGES     6         // how many config-pages (each has NDS_PER_PAGE definitions)

#define NDS_COUNT (NDS_PER_PAGE*NDS_PAGES)    // overall number of NDS definitions

#define AWAR_NDS_USE_ALL "arb_presets/all"
#define AWAR_NDS_PAGE    "arb_presets/page"

class NodeTextBuilder : virtual Noncopyable {
    GBS_strstruct out;

    long count;                 // number of active NDS entries
    int  show_errors;           // how many errors to show

    long  lengths[NDS_COUNT];   // allowed max. length of generated string (0 means "unlimited")
    char *fieldname[NDS_COUNT]; // database field name (if empty -> no field; may be hierarchical, see 'rek')
    bool  rek[NDS_COUNT];       // 1->key is hierarchical (e.g. 'ali_16s/data')
    char *parsing[NDS_COUNT];   // ACI/SRT program
    bool  at_group[NDS_COUNT];  // whether string shall appear at group NDS entries
    bool  at_leaf[NDS_COUNT];   // whether string shall appear at leaf NDS entries

    void append(const char *str, int length = -1) {
        nds_assert(str);
        if (length == -1) length = strlen(str);
        nds_assert(int(strlen(str)) == length);
        out.ncat(str, length);
    }

    __ATTR__FORMAT(2) void appendf(const char *format, ...) { FORWARD_FORMATTED(append, format); }

public:

    NodeTextBuilder() :
        out(200),
        count(-1)
    {
        for (int i = 0; i<NDS_COUNT; ++i) {
            fieldname[i] = NULp;
            parsing[i]   = NULp;
        }
    }
    ~NodeTextBuilder();

    void        init(GBDATA *gb_main);
    const char *work(GBDATA *gb_main, GBDATA *gbd, NDS_Type mode, TreeNode *species, const char *tree_name, bool forceGroup);

    long max_columns() const { // number of used NDS entries (>= max number of columns in generated value)
        nds_assert(count>=0);
        return count;
    }
};

inline const char *viewkeyAwarName(int i, const char *name) {
    nds_assert(i >= 0 && i < NDS_PER_PAGE);
    return GBS_global_string("tmp/viewkeys/viewkey_%i/%s", i, name);
}

inline AW_awar *viewkeyAwar(AW_root *aw_root, AW_default awdef, int i, const char *name, bool string_awar) {
    const char *awar_name = viewkeyAwarName(i, name);
    AW_awar    *awar      = NULp;
    if (string_awar) awar = aw_root->awar_string(awar_name, "", awdef);
    else        awar      = aw_root->awar_int(awar_name, 0, awdef);
    return awar;
}

#define VIEWKEY_LENGTH_MAX 1000000 // do not change (will break backward compatibility!)

static void nds_length_changed_cb(AW_root *, AW_awar *Awar_len1, AW_awar *Awar_len2) {
    long len2 = Awar_len2->read_int();
    if (len2<0 || len2>=VIEWKEY_LENGTH_MAX) {
        len2 = 0;
        Awar_len2->write_int(len2);
    }
    long len1 = len2 ? len2 : VIEWKEY_LENGTH_MAX;
    Awar_len1->write_int(len1);
}

static void map_viewkey(AW_root *aw_root, AW_default awdef, int i, GBDATA *gb_viewkey, bool initialize) {
    // maps one NDS key data to one line of the config window.
    // if 'initialize' is true => add callbacks etc. (shall only happen once for each 'i')

    GBDATA *gb_key_text = GB_entry(gb_viewkey, "key_text");
    GBDATA *gb_pars     = GB_entry(gb_viewkey, "pars");
    GBDATA *gb_group    = GB_entry(gb_viewkey, "group");
    GBDATA *gb_leaf     = GB_entry(gb_viewkey, "leaf");
    GBDATA *gb_len1     = GB_entry(gb_viewkey, "len1");
    GBDATA *gb_len2     = GB_entry(gb_viewkey, "len2");

    nds_assert(gb_key_text);
    nds_assert(gb_pars);
    nds_assert(gb_group);
    nds_assert(gb_leaf);
    nds_assert(gb_len1);
    nds_assert(gb_len2);

    {
        AW_awar *Awar;
        Awar = viewkeyAwar(aw_root, awdef, i, "key_text", true); Awar->map(gb_key_text);
        Awar = viewkeyAwar(aw_root, awdef, i, "pars",     true); Awar->map(gb_pars);
        Awar = viewkeyAwar(aw_root, awdef, i, "group",    false); Awar->map(gb_group);
        Awar = viewkeyAwar(aw_root, awdef, i, "leaf",     false); Awar->map(gb_leaf);
    }

    AW_awar *Awar_len1 = viewkeyAwar(aw_root, awdef, i, "len1", false); Awar_len1->map(gb_len1);
    AW_awar *Awar_len2 = viewkeyAwar(aw_root, awdef, i, "len2", false); Awar_len2->map(gb_len2);

    if (initialize) {
        Awar_len2->add_callback(makeRootCallback(nds_length_changed_cb, Awar_len1, Awar_len2));
    }
}

static void map_viewkeys(AW_root *aw_root, /*AW_default*/ GBDATA *awdef, GBDATA *gb_main) {
    // map visible viewkeys to internal db entries
    static bool  initialized        = false;
    AW_awar     *awar_selected_page = NULp;

    if (!initialized) {
        awar_selected_page = aw_root->awar_int(AWAR_NDS_PAGE, 0, gb_main);
        awar_selected_page->add_callback(makeRootCallback(map_viewkeys, awdef, gb_main)); // bind to self
    }
    else {
        awar_selected_page = aw_root->awar(AWAR_NDS_PAGE);
    }

    int page = awar_selected_page->read_int();
    if (page<NDS_PAGES) {
        GB_transaction ta(gb_main);

        GBDATA *gb_arb_presets = GB_search(gb_main, "arb_presets", GB_CREATE_CONTAINER);
        GBDATA *gb_viewkey     = NULp;

        int i1 = page*NDS_PER_PAGE;
        int i2 = i1+NDS_PER_PAGE-1;

        for (int i = 0; i <= i2; i++) {
            gb_viewkey = gb_viewkey ? GB_nextEntry(gb_viewkey) : GB_entry(gb_arb_presets, "viewkey");
            nds_assert(i<NDS_COUNT);
            nds_assert(gb_viewkey);
            if (i >= i1) map_viewkey(aw_root, awdef, i-i1, gb_viewkey, !initialized);
        }
    }

    initialized = true;
}

static __ATTR__USERESULT GB_ERROR nds_delete_database_entries(GBDATA *gb_main, int page_to_reset, bool only_delete_len2_entries) {
    GB_transaction  ta(gb_main);
    GBDATA         *gb_arb_presets = GB_search(gb_main, "arb_presets", GB_FIND);
    GB_ERROR        error          = NULp;

    if (gb_arb_presets) { // existing NDS settings => delete
        GBDATA *gb_viewkey = NULp;

        for (int i = 0; i<NDS_COUNT; ++i) {
            gb_viewkey = gb_viewkey ? GB_nextEntry(gb_viewkey) : GB_entry(gb_arb_presets, "viewkey");

            int current_page = i/NDS_PER_PAGE;

            if (!gb_viewkey) {
                // whole entry does not exist -> create it
                gb_viewkey             = GB_create_container(gb_arb_presets, "viewkey");
                if (!gb_viewkey) error = GB_await_error();
            }
            else if (current_page == page_to_reset) {
                // existing NDS entry on current page => delete contained entries
                for (GBDATA *gb_child = GB_child(gb_viewkey); gb_child && !error; ) {
                    GBDATA *gb_next_child = GB_nextChild(gb_child);

                    bool del = true;
                    if (only_delete_len2_entries) {
                        const char *key     = GB_read_key_pntr(gb_child);
                        bool        is_len2 = key && strcmp(key, "len2") == 0;
                        del = is_len2;
                    }

                    if (del) error = GB_delete(gb_child);
                    gb_child       = gb_next_child;
                }
            }
        }
    }

    return ta.close(error);
}

static __ATTR__USERESULT GB_ERROR nds_maintain_viewkeys(GBDATA *gb_main) {
    // Note: shall be callable multiple times
    // (consecutive calls with same DB should not change anything).

    GB_transaction  ta(gb_main);
    GBDATA         *gb_viewkey     = NULp;
    GBDATA         *gb_arb_presets = GB_search(gb_main, "arb_presets", GB_CREATE_CONTAINER);
    GB_ERROR        error          = gb_arb_presets ? NULp : GB_await_error();

    for (int i = 0; i<NDS_COUNT && !error; ++i) {
        gb_viewkey = gb_viewkey ? GB_nextEntry(gb_viewkey) : GB_entry(gb_arb_presets, "viewkey");

        if (!gb_viewkey) {
            gb_viewkey             = GB_create_container(gb_arb_presets, "viewkey");
            if (!gb_viewkey) error = GB_await_error();
        }

        if (!error) {
            int       group          = 0;
            int       leaf           = 0;
            bool      was_group_name = false;

            GBDATA *gb_key_text = GB_entry(gb_viewkey, "key_text");
            if (!gb_key_text) {
                gb_key_text             = GB_create(gb_viewkey, "key_text", GB_STRING);
                if (!gb_key_text) error = GB_await_error();
                else              {
                    const char *wanted = "";
                    switch (i) {
                        case 0: wanted = "name"; leaf = 1; break;
                        case 1: wanted = "full_name"; leaf = 1; break;
                        case 2: wanted = ""; was_group_name = true; break;
                        case 3: wanted = "acc"; leaf = 1; break;
                        case 4: wanted = "date"; break;
                    }
                    error = GB_write_string(gb_key_text, wanted);
                }
            }

            if (!error && strcmp(GB_read_char_pntr(gb_key_text), "group_name") == 0) {
                error          = GB_write_string(gb_key_text, "");
                was_group_name = true; // means: change group/leaf + add 'taxonomy(1)' to ACI
            }

            if (!error) {
                GBDATA *gb_pars          = GB_searchOrCreate_string(gb_viewkey, "pars", "");
                if      (!gb_pars) error = GB_await_error();
                else if (was_group_name) {
                    group = 1;
                    leaf  = 0;

                    const char *pars = GB_read_char_pntr(gb_pars);

                    if (pars[0] == 0) pars        = "taxonomy(1)"; // empty ACI/SRT
                    else if (pars[0] == ':') pars = GBS_global_string("taxonomy(1)|%s", pars); // was an SRT -> unsure what to do
                    else if (pars[0] == '|') pars = GBS_global_string("taxonomy(1)%s", pars); // was an ACI -> prefix taxonomy
                    else pars                     = GBS_global_string("taxonomy(1)|%s", pars); // other ACIs -> same

                    error = GB_write_string(gb_pars, pars);
                }
            }

            if (!error) {
                GBDATA *gb_flag1 = GB_entry(gb_viewkey, "flag1");
                if (gb_flag1) {
                    if (GB_read_int(gb_flag1)) { // obsolete
                        leaf = 1;
                    }
                    error = GB_delete(gb_flag1);
                }
            }

            if (!error) {
                GBDATA *gb_inherit = GB_entry(gb_viewkey, "inherit");
                if (gb_inherit) { // 'inherit' is old NDS style -> convert & delete
                    if (was_group_name && GB_read_int(gb_inherit)) leaf = 1;
                    error = GB_delete(gb_inherit);
                }
            }

            if (!error) {
                GBDATA *gb_group     = GB_searchOrCreate_int(gb_viewkey, "group", group);
                if (!gb_group) error = GB_await_error();
                else    group        = GB_read_int(gb_group);
            }
            if (!error) {
                GBDATA *gb_leaf     = GB_searchOrCreate_int(gb_viewkey, "leaf", leaf);
                if (!gb_leaf)  error = GB_await_error();
                else    leaf         = GB_read_int(gb_leaf);
            }

            // maintain length field
            if (!error) {
                GBDATA *gb_len1     = GB_searchOrCreate_int(gb_viewkey, "len1", VIEWKEY_LENGTH_MAX); // length field used up to arb-7.0 (kept up-to-date for backward compatibility)
                if (!gb_len1) error = GB_await_error();
                else {
                    long    arb7value   = GB_read_int(gb_len1);
                    long    DEFAULT_LEN = 0;                                                             // 0 means "unlimited"
                    GBDATA *gb_len2     = GB_entry(gb_viewkey, "len2");                                  // length field used starting with arb-7.1

                    if (!gb_len2) {
                        // first time this database is started with arb-7.1++
                        // (also happens after loading a stored config => keep configs arb-7.0-- compatible)
                        if (arb7value != VIEWKEY_LENGTH_MAX) {
                            bool userWantedUnlimited = arb7value >= 999;
                            bool isDefaultAndUnused  = arb7value == 30 && !leaf && !group;
                            if (userWantedUnlimited || isDefaultAndUnused) {
                                error = GB_write_int(gb_len1, VIEWKEY_LENGTH_MAX);
                            }
                            else {
                                DEFAULT_LEN = arb7value;
                            }
                        }

                        if (!error) {
                            gb_len2 = GB_searchOrCreate_int(gb_viewkey, "len2", DEFAULT_LEN);
                            if (!gb_len2) {
                                error = GB_await_error();
                            }
                        }
                    }
                    else {
                        // always use backward-compat-value in "len1"
                        // - either that value was correctly maintained by arb-7.1++ or
                        // - database was edited with arb-7.0-- and the length value was changed there

                        long wanted = arb7value == VIEWKEY_LENGTH_MAX ? DEFAULT_LEN : arb7value;

                        if (GB_read_int(gb_len2) != wanted) {
                            error = GB_write_int(gb_len2, wanted);
                        }
                    }
                }
            }
        }
    }

    return ta.close(error);
}

static __ATTR__USERESULT GB_ERROR nds_create_vars(AW_root *aw_root, AW_default awdef, GBDATA *gb_main) {
    GB_transaction ta(gb_main);
    aw_root->awar_int(AWAR_NDS_USE_ALL, 1, gb_main);

    GB_ERROR error = nds_maintain_viewkeys(gb_main);
    if (!error) map_viewkeys(aw_root, awdef, gb_main);
    return ta.close(error);
}

void NDS_create_vars(AW_root *aw_root, AW_default awdef, GBDATA *gb_main) {
    aw_message_if(nds_create_vars(aw_root, awdef, gb_main));
}

static const char *script_part_of(const char *predef_entry) {
    const char *numsign = strchr(predef_entry, '#');
    return numsign ? numsign+1 : predef_entry;
}

static bool in_pre_update = false;

static void awt_pre_to_view(AW_root *aw_root) {
    if (!in_pre_update) {
        LocallyModify<bool> dontRecurse(in_pre_update, true);

        const char *sel_predef_entry = aw_root->awar(AWAR_SELECT_ACISRT_PRE)->read_char_pntr();
        aw_root->awar(AWAR_SELECT_ACISRT)->write_string(script_part_of(sel_predef_entry));
    }
}
static void awt_select_pre_from_view(AW_root *aw_root, AW_selection_list *programs) {
    if (!in_pre_update) {
        LocallyModify<bool> dontRecurse(in_pre_update, true);

        const char *currScript  = aw_root->awar(AWAR_SELECT_ACISRT)->read_char_pntr();
        const char *foundPredef = NULL;

        for (AW_selection_list_iterator piter(programs); piter && !foundPredef; ++piter) { // search entry pre-defining current script
            const char *predef = piter.get_value()->get_string();
            const char *script = script_part_of(predef);

            if (strcmp(script, currScript) == 0) {
                foundPredef = predef;
            }
        }

        // select script pre-defining current ACI (or select default if modified)
        aw_root->awar(AWAR_SELECT_ACISRT_PRE)->write_string(foundPredef ? foundPredef : currScript);
    }
}

void NDS_popup_select_srtaci_window(AW_window *aww, const char *acisrt_awarname) {
    static AW_window *win = NULp;

    AW_root *aw_root = aww->get_root();

    if (!win) {
        AW_awar *awar_curr_aci = aw_root->awar_string(AWAR_SELECT_ACISRT);
        AW_awar *awar_sel_aci  = aw_root->awar_string(AWAR_SELECT_ACISRT_PRE);

        AW_window_simple *aws = new AW_window_simple;
        aws->init(aw_root, "SRT_ACI_SELECT", "SRT_ACI_SELECT");
        aws->load_xfig("awt/srt_select.fig");

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

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

        aws->at("box");
        AW_selection_list *programs = aws->create_selection_list(AWAR_SELECT_ACISRT_PRE);
        GB_ERROR           error;
        {
            StorableSelectionList storable_sellist(TypedSelectionList("sellst", programs, "SRT/ACI scripts", "srt_aci"));
            error = storable_sellist.load(GB_path_in_ARBLIB("sellists/srt_aci*.sellst"), false);
        }
        if (error) aw_message(error);

        aws->at("field");
        aws->create_text_field(AWAR_SELECT_ACISRT);

        awar_sel_aci->add_callback(awt_pre_to_view);
        awar_curr_aci->add_callback(makeRootCallback(awt_select_pre_from_view, programs));

        win = aws;
    }

    aw_root->awar(AWAR_SELECT_ACISRT)->map(acisrt_awarname);
    win->activate();
}

static void nds_init_config(AWT_config_definition& cdef) {
    for (int i = 0; i<NDS_PER_PAGE; ++i) {
        cdef.add(viewkeyAwarName(i, "leaf"), "leaf", i);
        cdef.add(viewkeyAwarName(i, "group"), "group", i);
        cdef.add(viewkeyAwarName(i, "key_text"), "key_text", i);
        cdef.add(viewkeyAwarName(i, "len1"), "len1", i); // continue to use old arb-7.0-- config name (after loading config, nds_maintain_viewkeys() will correct values again)
        cdef.add(viewkeyAwarName(i, "pars"), "pars", i);
    }
}

static char *nds_store_config() {
    AWT_config_definition cdef;
    nds_init_config(cdef);
    return cdef.read();
}

static void nds_restore_config(const char *stored, GBDATA *gb_main) {
    // if stored == NULp -> reset

    AWT_config_definition cdef;
    nds_init_config(cdef);

    const int current_page = AW_root::SINGLETON->awar(AWAR_NDS_PAGE)->read_int();

    if (stored) { // restore
        AWT_config parsedCfg(stored);
        if (parsedCfg.has_entry("inherit0")) {
            aw_message("Converting stored config to new NDS format -- consider saving it again.");
            // Note: The conversion applied here is also done in NDS_create_vars()

            GB_ERROR error = NULp;

            for (int i = 0; !error && i<NDS_COUNT; ++i) {
                bool was_group_name = false;
                {
                    const char *key_text_key = GBS_global_string("key_text%i", i);
                    const char *key_text     = parsedCfg.get_entry(key_text_key);
                    if (strcmp(key_text, "group_name") == 0) {
                        was_group_name = true;
                        parsedCfg.set_entry(key_text_key, "");
                    }
                }

                bool leaf    = false;
                bool group   = false;
                int  inherit = 0;

                {
                    const char *inherit_key   = GBS_global_string("inherit%i", i);
                    const char *inherit_value = parsedCfg.get_entry(inherit_key);

                    if (inherit_value) {
                        inherit = atoi(inherit_value);
                        parsedCfg.delete_entry(inherit_key);
                    }
                    else {
                        error = GB_export_errorf("Expected entry '%s' in saved config", inherit_key);
                    }
                }

                if (was_group_name) {
                    if (!error) {
                        leaf  = inherit;
                        group = true;

                        char       *aci_key = GBS_global_string_copy("pars%i", i);
                        const char *aci     = parsedCfg.get_entry(aci_key);
                        char       *new_aci = NULp;

                        if      (aci[0] == 0)   { new_aci = strdup("taxonomy(1)"); }
                        else if (aci[0] == '|') { new_aci = GBS_global_string_copy("taxonomy(1)%s", aci); }
                        else                    { new_aci = GBS_global_string_copy("taxonomy(1)|%s", aci); }

                        parsedCfg.set_entry(aci_key, new_aci);

                        free(new_aci);
                        free(aci_key);
                    }
                }
                else {
                    leaf = true;
                }

                if (!error) {
                    const char *flag1_key   = GBS_global_string("active%i", i);
                    const char *flag1_value = parsedCfg.get_entry(flag1_key);
                    if (flag1_value) {
                        int flag1 = atoi(flag1_value);
                        if (flag1 == 0) { leaf = group = false; }
                        parsedCfg.delete_entry(flag1_key);
                    }
                    else {
                        error = GB_export_errorf("Expected entry '%s' in saved config", flag1_key);
                    }
                }

                if (!error) {
                    const char *leaf_key  = GBS_global_string("leaf%i", i);
                    parsedCfg.set_entry(leaf_key, GBS_global_string("%i", int(leaf)));
                    const char *group_key = GBS_global_string("group%i", i);
                    parsedCfg.set_entry(group_key, GBS_global_string("%i", int(group)));
                }
            }

            if (!error) {
                char *converted_cfg_str = parsedCfg.config_string();
                cdef.write(converted_cfg_str);
                free(converted_cfg_str);
            }
            else {
                aw_message(error);
            }
        }
        else {
            GB_transaction ta(gb_main); // avoid multiple refreshes of tree display during restore

            cdef.write(stored);

            GB_ERROR error    = nds_delete_database_entries(gb_main, current_page, true);      // removes "len2" DB entries of current page (10 entries) => next step does smart conversion
            if (!error) error = nds_create_vars(AW_root::SINGLETON, AW_ROOT_DEFAULT, gb_main); // => reinit NDS
            error             = ta.close(error);
            aw_message_if(error);
        }
    }
    else { // reset to factory defaults
        GB_transaction ta(gb_main); // bundles delete and recreate (refresh delayed until both is done)

        cdef.reset(); // AWAR values are just empty here

        GB_ERROR error    = nds_delete_database_entries(gb_main, current_page, false);     // removes DB entries of current page (10 entries)
        if (!error) error = nds_create_vars(AW_root::SINGLETON, AW_ROOT_DEFAULT, gb_main); // => reinit NDS
        error             = ta.close(error);
        aw_message_if(error);
    }
}

AW_window *NDS_create_window(AW_root *aw_root, GBDATA *gb_main) {
    static AW_window_simple *aws = NULp;
    if (!aws) {
        aws = new AW_window_simple;
        aws->init(aw_root, "NDS_PROPS", "NDS");
        aws->load_xfig("awt/nds.fig");
        aws->auto_space(10, 5);

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

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

        aws->at("page");
        aws->create_option_menu(AWAR_NDS_PAGE);
        for (int p = 0; p < NDS_PAGES; p++) {
            const char *text = GBS_global_string("Entries %i - %i", p*NDS_PER_PAGE+1, (p+1)*NDS_PER_PAGE);
            aws->insert_option(text, "", p);
        }
        aws->update_option_menu();

        aws->at("use");
        aws->create_option_menu(AWAR_NDS_USE_ALL);
        aws->insert_default_option("Use all entries",          "", 1);
        aws->insert_option        ("Only use visible entries", "", 0);
        aws->update_option_menu();

        aws->at("config");
        AWT_insert_config_manager(aws, AW_ROOT_DEFAULT, "nds", makeStoreConfigCallback(nds_store_config), makeRestoreConfigCallback(nds_restore_config, gb_main));

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

        aws->button_length(13);
        int dummy, closey;
        aws->at_newline();
        aws->get_at_position(&dummy, &closey);

        aws->create_button(NULp, "K");

        aws->at_newline();

        int leafx, groupx, fieldx, columnx, srtx, srtux;

        aws->auto_space(10, 0);

        int i;
        for (i=0; i<NDS_PER_PAGE; i++) {
            aws->get_at_position(&leafx, &dummy);
            aws->create_toggle(viewkeyAwarName(i, "leaf"));

            aws->get_at_position(&groupx, &dummy);
            aws->create_toggle(viewkeyAwarName(i, "group"));

            aws->get_at_position(&fieldx, &dummy);
            {
                const char *awar_name = viewkeyAwarName(i, "key_text");
                create_itemfield_selection_button(aws, FieldSelDef(awar_name, gb_main, SPECIES_get_selector(), FIELD_FILTER_NDS, "display-field"), NULp);
            }

            aws->get_at_position(&columnx, &dummy);
            aws->create_input_field(viewkeyAwarName(i, "len2"), 4);

            aws->get_at_position(&srtx, &dummy);
            {
                char *awar_name = strdup(viewkeyAwarName(i, "pars"));

                aws->button_length(0);
                aws->callback(makeWindowCallback(NDS_popup_select_srtaci_window, awar_name)); // awar_name belongs to cbs now
                {
                    char *button_id = GBS_global_string_copy("SELECT_SRTACI_%i", i+1);
                    aws->create_button(button_id, "S");
                    free(button_id);
                }

                aws->get_at_position(&srtux, &dummy);
                aws->at_attach_to(true, false, -7, 30);
                aws->create_input_field(awar_name, 40);
            }

            aws->at_unattach();
            aws->at_newline();
        }

        aws->at(leafx, closey);

        aws->at_x(leafx);
        aws->create_button(NULp, "LEAF");
        aws->at_x(groupx);
        aws->create_button(NULp, "GRP.");

        aws->at_x(fieldx);
        aws->create_button(NULp, "FIELD");

        aws->at_x(columnx);
        aws->create_button(NULp, "WIDTH");

        aws->at_x(srtx);
        aws->create_button(NULp, "SRT");

        aws->at_x(srtux);
        aws->create_button(NULp, "ACI/SRT PROGRAM");
    }

    return aws;
}



void NodeTextBuilder::init(GBDATA *gb_main) {
    GBDATA *gb_arb_presets    = GB_search(gb_main, "arb_presets", GB_CREATE_CONTAINER);
    bool    only_visible_page = false;
    int     page              = 0;
    {
        int all = *GBT_readOrCreate_int(gb_arb_presets, "all", 1);
        if (!all) {
            page              = *GBT_readOrCreate_int(gb_arb_presets, "page", 0);
            only_visible_page = true;
        }
    }

    count   = 0;
    int idx = 0;
    for (GBDATA *gbz = GB_entry(gb_arb_presets, "viewkey"); gbz; gbz = GB_nextEntry(gbz), ++idx) {
        bool use = !only_visible_page || (idx/NDS_PER_PAGE) == page;
        if (use) {
            // wanted NDS entry?
            bool atLeaf  = *GBT_read_int(gbz, "leaf");
            bool atGroup = *GBT_read_int(gbz, "group");

            if (atLeaf || atGroup) {
                GBDATA *gb_keyname = GB_entry(gbz, "key_text");
                char   *keyname    = GB_read_string(gb_keyname);

                if (keyname[0] && strcmp(keyname, NO_FIELD_SELECTED) == 0) {
                    freeset(keyname, strdup("")); // NDS code interprets empty keyname as "no field"
                }
                freeset(fieldname[count], keyname);

                rek[count]      = bool(GB_first_non_key_char(keyname));
                lengths[count]  = *GBT_read_int(gbz, "len2");
                at_leaf[count]  = atLeaf;
                at_group[count] = atGroup;

                GBDATA *gbe = GB_entry(gbz, "pars");
                freenull(parsing[count]);
                if (gbe && GB_read_string_count(gbe)>1) parsing[count] = GB_read_string(gbe);
                count++;
            }
        }
    }

    show_errors = 10;
}

NodeTextBuilder::~NodeTextBuilder() {
    for (int i = 0; i<count; ++i) {
        freenull(fieldname[i]);
        freenull(parsing[i]);
    }
}

static char *quoted_if_containing_separator(const char *text, char separator) {
    bool contains_separator = strchr(text, separator);
    if (!contains_separator) return NULp;
    return GBS_global_string_copy("\"%s\"", text);
}

const char *NodeTextBuilder::work(GBDATA *gb_main, GBDATA *gbd, NDS_Type mode, TreeNode *species, const char *tree_name, bool forceGroup) {
    // @@@ change result into SizedCstr? (to speed up display)

    nds_assert(count>=0); // initialized?
    out.erase();

    if (!gbd) {
        if (!species) return "<internal error: no tree-node, no db-entry>";
        if (!species->name) return "<internal error: species w/o name>";
        appendf("<%s>", species->name); // zombie
    }
    else {
        bool field_was_printed = false;
        bool is_leaf           = !forceGroup && (species ? species->is_leaf() : true);

        for (int i = 0; i < count; i++) {
            if (is_leaf) { if (!at_leaf[i]) continue; }
            else         { if (!at_group[i]) continue; }

            char *str        = NULp;  // the generated string
            bool  apply_aci  = false; // whether aci shall be applied

            {
                const char *field_output = "";
                const char *field_name   = fieldname[i];

                if (field_name[0] == 0) { // empty field_name (or NO_FIELD_SELECTED) -> only do ACI/SRT
                    apply_aci = true;
                }
                else { // non-empty field_name
                    GBDATA *gbe;
                    if (rek[i]) {       // hierarchical key
                        gbe = GB_search(gbd, field_name, GB_FIND);
                    }
                    else {              // flat entry
                        gbe = GB_entry(gbd, field_name);
                    }

                    // silently ignore missing fields (and leave apply_aci false!)
                    if (gbe) {
                        apply_aci = true;
                        switch (GB_read_type(gbe)) {
                            case GB_INT: field_output  = GBS_global_string("%li", GB_read_int(gbe)); break;
                            case GB_BYTE: field_output = GBS_global_string("%i", GB_read_byte(gbe)); break;

                            case GB_FLOAT: {
                                const char *format = "%5.4f";
                                if (mode == NDS_OUTPUT_TAB_SEPARATED) { // '.' -> ','
                                    char *dotted  = GBS_global_string_copy(format, GB_read_float(gbe));
                                    char *dot     = strchr(dotted, '.');
                                    if (dot) *dot = ',';
                                    field_output  = GBS_static_string(dotted);
                                    free(dotted);
                                }
                                else {
                                    field_output = GBS_global_string(format, GB_read_float(gbe));
                                }
                                break;
                            }
                            case GB_STRING:
                                field_output = GB_read_char_pntr(gbe);
                                if (!field_output) field_output="<read error>";
                                break;

                            default: {
                                char *as_string = GB_read_as_string(gbe);
                                field_output    = GBS_static_string(as_string);
                                free(as_string);
                            }
                        }
                    }
                    else {
                        if (GB_have_error()) {
                            field_output = GB_await_error();
                        }
                    }
                }
                str = strdup(field_output);
            }

            // apply ACI/SRT program

            GB_ERROR error = NULp;
            if (apply_aci) {
                const char *aci = parsing[i];
                if (aci) {
                    GBL_env      env(gb_main, tree_name);  // @@@ pass from caller?
                    GBL_call_env callEnv(gbd, env);

                    char *aci_result            = GB_command_interpreter_in_env(str, aci, callEnv);
                    if (!aci_result) aci_result = GBS_global_string_copy("<error: %s>", GB_await_error());
                    freeset(str, aci_result);
                }
            }

            NDS_mask_nonprintable_chars(str);

            // quote string, if it contains separator
            {
                char *quoted = NULp;
                switch (mode) {
                    case NDS_OUTPUT_COMMA_SEPARATED:
                        quoted = quoted_if_containing_separator(str, ',');
                        break;

                    case NDS_OUTPUT_TAB_SEPARATED:
                        quoted = quoted_if_containing_separator(str, '\t');
                        break;

                    case NDS_OUTPUT_LEAFTEXT:
                    case NDS_OUTPUT_LEAFTEXT_UNLIMITED:
                        break;
                }

                if (quoted) freeset(str, quoted);
            }


            bool skip_display = ((mode == NDS_OUTPUT_LEAFTEXT || mode == NDS_OUTPUT_LEAFTEXT_UNLIMITED) && str[0] == 0);
            if (!skip_display) {
                switch (mode) {
                    case NDS_OUTPUT_LEAFTEXT:
                    case NDS_OUTPUT_LEAFTEXT_UNLIMITED:
                        if (!field_was_printed) break; // no comma no space if nothing printed yet
                        out.put(','); // separate single fields by comma in compressed mode
                        out.put(' '); // print at least one space if not using tabs
                        break;

                    case NDS_OUTPUT_COMMA_SEPARATED:
                        if (i != 0) out.put(','); // CSV output (for office calc)
                        break;

                    case NDS_OUTPUT_TAB_SEPARATED:
                        if (i != 0) out.put('\t'); // tabbed output (for office calc)
                        break;
                }

                field_was_printed = true;

                int str_len = strlen(str);
                switch (mode) {
                    case NDS_OUTPUT_TAB_SEPARATED:
                    case NDS_OUTPUT_COMMA_SEPARATED:
                    case NDS_OUTPUT_LEAFTEXT_UNLIMITED:
                        append(str, str_len);
                        break;

                    case NDS_OUTPUT_LEAFTEXT: {
                        int nds_len = lengths[i];
                        if (str_len>nds_len && nds_len != 0) { // string is too long -> shorten (0 means "unlimited")
                            str[nds_len] = 0;
                            str_len      = nds_len;
                        }
                        append(str, str_len);
                    }
                }
            }

            // show first XXX errors
            if (error && show_errors>0) {
                show_errors--;
                aw_message(error);
            }

            free(str);
        }
    }

    return out.get_data();
}

NodeTextBuilder& NDS_Labeler::theBuilder(GBDATA *gb_main) const {
    if (!builder) {
        aw_message_if(nds_maintain_viewkeys(gb_main));
        builder = new NodeTextBuilder;
        builder->init(gb_main);
    }
    return *builder;
}

NDS_Labeler::NDS_Labeler(NDS_Type type_) :
    type(type_),
    builder(NULp)
{
}
NDS_Labeler::~NDS_Labeler() {
    delete builder;
}

// @@@ add method returning a string array (e.g. ConstStrArray?) instead of comma and/or tab separated versions. useful when caller splits string again.

long NDS_Labeler::max_columns(GBDATA *gb_main) const {
    return theBuilder(gb_main).max_columns();
}

const char *NDS_Labeler::speciesLabel(GBDATA *gb_main, GBDATA *gb_species, TreeNode *species, const char *tree_name) const {
    return theBuilder(gb_main).work(gb_main, gb_species, get_NDS_Type(), species, tree_name, false);
}
const char *NDS_Labeler::groupLabel(GBDATA *gb_main, GBDATA *gb_group, TreeNode *species, const char *tree_name) const {
    // forces group nodes (even if 'species' is a leaf)
    return theBuilder(gb_main).work(gb_main, gb_group, get_NDS_Type(), species, tree_name, true);
}

static const char *createReplaceTable() {
    static char replaceTable[256];

    for (int i = 0; i<32; ++i)   replaceTable[i] = '?';
    for (int i = 32; i<256; ++i) replaceTable[i] = i;

    const char LFREP = '#';

    replaceTable['\n'] = LFREP;
    replaceTable['\r'] = LFREP;
    replaceTable['\t'] = ' ';

    return replaceTable;
}

char *NDS_mask_nonprintable_chars(char * const str) {
    // mask nonprintable characters in result of NDS.
    //
    // modifies and returns 'str'
    //
    // background: gtk renders LFs as such (i.e. renders multiple lines),
    //             motif printed a small box (i.e. rendered all text in one line)

    static const char *replaceTable = createReplaceTable();
    for (int i = 0; str[i]; ++i) {
        str[i] = replaceTable[safeCharIndex(str[i])];
    }
    return str;
}

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

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

#define TEST_EXPECT_MASK_NONPRINTABLE(i,o) do { \
        char *masked = strdup(i);               \
        NDS_mask_nonprintable_chars(masked);    \
        TEST_EXPECT_EQUAL(masked,o);            \
        free(masked);                           \
    } while (0)

void TEST_mask_nds() {
    TEST_EXPECT_MASK_NONPRINTABLE("plain text",     "plain text");
    TEST_EXPECT_MASK_NONPRINTABLE("with\nLF",       "with#LF");
    TEST_EXPECT_MASK_NONPRINTABLE("with\rLF",       "with#LF");
    TEST_EXPECT_MASK_NONPRINTABLE("with\r\nLF",     "with##LF");
    TEST_EXPECT_MASK_NONPRINTABLE("tab\tseparated", "tab separated");
    TEST_EXPECT_MASK_NONPRINTABLE("\t\n\t\n",       " # #");
}

#define TEST_EXPECT_NDS_EQUALS(specName,labeler,expected_NDS) do {                      \
        GBDATA *gb_species  = GBT_find_species(gb_main, specName);                      \
        TEST_REJECT_NULL(gb_species);                                                   \
                                                                                        \
        const char *nds = labeler.speciesLabel(gb_main, gb_species, NULp, NULp);        \
        TEST_EXPECT_EQUAL(nds, expected_NDS);                                           \
    } while(0)

#define TEST_EXPECT_NDS_EQUALS__BROKEN(specName,labeler,expected_NDS) do {              \
        GBDATA *gb_species  = GBT_find_species(gb_main, specName);                      \
        TEST_REJECT_NULL(gb_species);                                                   \
                                                                                        \
        const char *nds = labeler.speciesLabel(gb_main, gb_species, NULp, NULp);        \
        TEST_EXPECT_EQUAL__BROKEN(nds, expected_NDS);                                   \
    } while(0)

void TEST_nds() {
    GB_shell    shell;
    const char *testDB  = "display/nds.arb"; // NDS definitions are in ../../UNIT_TESTER/run/display/nds.arb@arb_presets
    GBDATA     *gb_main = GB_open(testDB, "r");

    TEST_REJECT_NULL(gb_main);

    {
        GB_transaction ta(gb_main);

        NDS_Labeler leaftext_labeler(NDS_OUTPUT_LEAFTEXT);
        NDS_Labeler leaftext_unlimited(NDS_OUTPUT_LEAFTEXT_UNLIMITED);
        NDS_Labeler tab_separated_labeler(NDS_OUTPUT_TAB_SEPARATED);
        NDS_Labeler comma_separated_labeler(NDS_OUTPUT_COMMA_SEPARATED);

        TEST_EXPECT_NDS_EQUALS("MycChlor", leaftext_labeler,        "'MycChlor', Mycobacterium #phenolicus, acc=X79094");   // missing field 'comment' not appended
        TEST_EXPECT_NDS_EQUALS("MycChlor", leaftext_unlimited,      "'MycChlor', Mycobacterium #phenolicus, acc=X79094");   // missing field 'comment' not appended
        TEST_EXPECT_NDS_EQUALS("MycChlor", tab_separated_labeler,   "'MycChlor'\tMycobacterium #phenolicus\tacc=X79094\t"); // but empty column for 'comment' inserted here
        TEST_EXPECT_NDS_EQUALS("MycChlor", comma_separated_labeler, "'MycChlor',Mycobacterium #phenolicus,acc=X79094,");    // and here

        TEST_EXPECT_NDS_EQUALS("ActUtahe", leaftext_labeler,        "'ActUtahe', Act;ino planes uta,hen.sis#, acc=X80823, comment");  // comment not truncated (unlimited zero width)
        TEST_EXPECT_NDS_EQUALS("ActUtahe", leaftext_unlimited,      "'ActUtahe', Act;ino planes uta,hen.sis#, acc=X80823, comment");  // comment not truncated (unlimited zero width)
        TEST_EXPECT_NDS_EQUALS("ActUtahe", tab_separated_labeler,   "'ActUtahe'\tAct;ino planes uta,hen.sis#\tacc=X80823\tcomment");
        TEST_EXPECT_NDS_EQUALS("ActUtahe", comma_separated_labeler, "'ActUtahe',\"Act;ino planes uta,hen.sis#\",acc=X80823,comment"); // quote 2nd value (cause it contains a comma)

        TEST_EXPECT_NDS_EQUALS("StpGrise", leaftext_labeler,        "'StpGrise', Strepto s griseus, acc=M76388 X55435 X6");       // acc truncated!
        TEST_EXPECT_NDS_EQUALS("StpGrise", leaftext_unlimited,      "'StpGrise', Strepto s griseus, acc=M76388 X55435 X61478");   // acc NOT truncated here
        TEST_EXPECT_NDS_EQUALS("StpGrise", tab_separated_labeler,   "'StpGrise'\tStrepto s griseus\tacc=M76388 X55435 X61478\t"); // acc NOT truncated here
        TEST_EXPECT_NDS_EQUALS("StpGrise", comma_separated_labeler, "'StpGrise',Strepto s griseus,acc=M76388 X55435 X61478,");

        TEST_EXPECT_NDS_EQUALS("StpAmbof", leaftext_labeler,        "'StpAmbof', Strepto s ambofaciens nomen fa, acc=M27245, This is the comment for StpAmbof :-)");                       // full_name truncated + comment not truncated (unlimited zero width)
        TEST_EXPECT_NDS_EQUALS("StpAmbof", leaftext_unlimited,      "'StpAmbof', Strepto s ambofaciens nomen falsus longus abscindere, acc=M27245, This is the comment for StpAmbof :-)"); // full_name + comment not truncated here
        TEST_EXPECT_NDS_EQUALS("StpAmbof", tab_separated_labeler,   "'StpAmbof'\tStrepto s ambofaciens nomen falsus longus abscindere\tacc=M27245\tThis is the comment for StpAmbof :-)"); // full_name + comment not truncated here
        TEST_EXPECT_NDS_EQUALS("StpAmbof", comma_separated_labeler, "'StpAmbof',Strepto s ambofaciens nomen falsus longus abscindere,acc=M27245,This is the comment for StpAmbof :-)");
    }

    GB_close(gb_main);
}

#endif // UNIT_TESTS

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





