// ========================================================= //
//                                                           //
//   File      : selection_admin.cxx                         //
//   Purpose   : species selection admin                     //
//                                                           //
//   Coded by Ralf Westram (coder@reallysoft.de) in Feb 26   //
//   http://www.arb-home.de/                                 //
//                                                           //
// ========================================================= //

#include "selection_admin.h"
#include <sel_boxes.hxx>

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

#include <arb_global_defs.h>
#include <arb_strarray.h>

#include <ad_config.h>
#include <ad_cb.h>
#include <arbdbt.h>
#include <RegExpr.hxx>

#include <TreeNode.h>
#include <modules.hxx>

#include <map>

using namespace std;

// -----------------------------
//      class Store_species

class Store_species : virtual Noncopyable {
    // stores an amount of species:
    TreeNode *node;
    Store_species *next;
public:
    Store_species(TreeNode *aNode) {
        node = aNode;
        next = NULp;
    }
    ~Store_species();

    Store_species* add(Store_species *list) {
        arb_assert(!next);
        next = list;
        return this;
    }

    Store_species* remove() {
        Store_species *follower = next;
        next = NULp;
        return follower;
    }

    TreeNode *getNode() const { return node; }

    void call(void (*aPizza)(TreeNode*)) const;
};

Store_species::~Store_species() {
    delete next;
}

void Store_species::call(void (*aPizza)(TreeNode*)) const {
    aPizza(node);
    if (next) next->call(aPizza);
}

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

static void unmark_species(TreeNode *node) {
    arb_assert(node);
    arb_assert(node->gb_node);
    arb_assert(GB_read_flag(node->gb_node)!=0);
    GB_write_flag(node->gb_node, 0);
}

static void mark_species(TreeNode *node, Store_species **extra_marked_species) {
    arb_assert(node);
    arb_assert(node->gb_node);
    arb_assert(GB_read_flag(node->gb_node)==0);
    GB_write_flag(node->gb_node, 1);

    *extra_marked_species = (new Store_species(node))->add(*extra_marked_species);
}

static TreeNode *rightmost_leaf(TreeNode *node) {
    arb_assert(node);
    while (!node->is_leaf()) {
        node = node->get_rightson();
        arb_assert(node);
    }
    return node;
}

static TreeNode *left_neighbour_leaf(TreeNode *node) {
    if (node) {
        TreeNode *father = node->get_father();
        while (father) {
            if (father->rightson==node) {
                node = rightmost_leaf(father->get_leftson());
                arb_assert(node->is_leaf());
                if (!node->gb_node) { // Zombie
                    node = left_neighbour_leaf(node);
                }
                return node;
            }
            node = father;
            father = node->get_father();
        }
    }
    return NULp;
}

const char CFG_SEP = 1;

static int nt_build_conf_string_rek(GB_HASH         *used,
                                    TreeNode        *tree,
                                    GBS_strstruct&   memfile,
                                    Store_species  **extra_marked_species,
                                    int              use_species_aside,
                                    int             *auto_mark,
                                    int              marked_at_left,
                                    int             *marked_at_right)
{
    /*! Builds a configuration string from a tree.
     *
     * @param used                      all species inserted by this function are stored here
     * @param tree                      used for group information
     * @param memfile                   generated configuration string is stored here
     * @param extra_marked_species      all extra marked species are inserted here
     * @param use_species_aside         number of species to mark left and right of marked species
     * @param auto_mark                 number species to extra-mark (if not already marked)
     * @param marked_at_left            number of species which were marked (looking to left)
     * @param marked_at_right           number of species which are marked (when returning from recursion)
     *
     * @return the number of marked species
     *
     * --------------------------------------------------
     * Format of configuration string : [Part]+ \0
     *
     * Part : '\A' ( Group | Species | Sai )
     *
     * Group : ( OpenedGroup | ClosedGroup )
     * OpenedGroup : 'G' GroupDef
     * ClosedGroup : 'F' GroupDef
     * GroupDef : 'groupname' [PART]* EndGroup
     * EndGroup : '\AE'
     *
     * SPECIES : 'L' 'speciesname'
     * SAI : 'S' 'sainame'
     *
     * \0 : ASCII 0 (eos)
     * \A : ASCII 1
     */

    if (!tree) return 0;
    if (tree->is_leaf()) {
        if (!tree->gb_node) {
            UNCOVERED();
            *marked_at_right = marked_at_left;
            return 0;   // Zombie
        }

        if (!GB_read_flag(tree->gb_node)) { // unmarked species
            if (*auto_mark) {
                (*auto_mark)--;
                mark_species(tree, extra_marked_species);
            }
            else {
                *marked_at_right = 0;
                return 0;
            }
        }
        else { // marked species
            if (marked_at_left<use_species_aside) {
                // on the left side there are not as many marked species as needed!

                arb_assert(marked_at_left>=0);

                TreeNode *leaf_at_left = tree;
                int       step_over    = marked_at_left+1; // step over myself
                int       then_mark    = use_species_aside-marked_at_left;

                while (step_over--) { // step over self and over any adjacent, marked species
                    leaf_at_left = left_neighbour_leaf(leaf_at_left);
                }

                Store_species *marked_back = NULp;
                while (leaf_at_left && then_mark--) { // then additionally mark some species
                    if (GB_read_flag(leaf_at_left->gb_node) == 0) { // if they are not marked yet
                        mark_species(leaf_at_left, extra_marked_species);
                        marked_back = (new Store_species(leaf_at_left))->add(marked_back);
                    }
                    leaf_at_left = left_neighbour_leaf(leaf_at_left);
                }

                while (marked_back) {
                    memfile.put(CFG_SEP);
                    memfile.put('L');
                    memfile.cat(marked_back->getNode()->name);
                    GBS_write_hash(used, marked_back->getNode()->name, 1);      // Mark species

                    Store_species *rest = marked_back->remove();
                    delete marked_back;
                    marked_back = rest;
                }

                marked_at_left = use_species_aside;
            }
            // now use_species_aside species to left are marked!
            *auto_mark = use_species_aside;
        }

        memfile.put(CFG_SEP);
        memfile.put('L');
        memfile.cat(tree->name);
        GBS_write_hash(used, tree->name, 1);    // Mark species

        *marked_at_right = marked_at_left+1;
        return 1;
    }

    const size_t oldpos = memfile.get_position();
    if (tree->gb_node && tree->name) {      // but we are a group
        GBDATA *gb_grouped = GB_entry(tree->gb_node, "grouped");
        memfile.put(CFG_SEP);
        if (gb_grouped && GB_read_byte(gb_grouped)) {
            memfile.put('F');
        }
        else {
            memfile.put('G');
        }

        memfile.cat(tree->name);
    }

    int  right_of_leftson;
    long nspecies=   nt_build_conf_string_rek(used, tree->get_leftson(),  memfile, extra_marked_species, use_species_aside, auto_mark, marked_at_left,   &right_of_leftson);
    nspecies      += nt_build_conf_string_rek(used, tree->get_rightson(), memfile, extra_marked_species, use_species_aside, auto_mark, right_of_leftson, marked_at_right);

    if (tree->gb_node && tree->name) {      // but we are a group
        memfile.put(CFG_SEP);
        memfile.put('E');        // Group end indicated by 'E'
    }

    if (!nspecies) {
        const size_t newpos = memfile.get_position();
        memfile.cut_tail(newpos-oldpos); // delete group info
    }
    return nspecies;
}

struct SAI_string_builder {
    GBS_strstruct&  sai_middle;
    const char     *last_group_name;
};

static void nt_build_sai_string_by_hash(const char *key, long /*val*/, void *cd_sai_builder) {
    SAI_string_builder *sai_builder = (SAI_string_builder*)cd_sai_builder;

    const char *sep = strchr(key, 1);
    if (sep) {
        GBS_strstruct& sai_middle      = sai_builder->sai_middle;
        const char    *last_group_name = sai_builder->last_group_name;

        if (!last_group_name || strncmp(key, last_group_name, sep-key)) { // new group
            if (last_group_name) {
                sai_middle.put(CFG_SEP);
                sai_middle.put('E');             // End of old group
            }
            sai_middle.put(CFG_SEP);
            sai_middle.cat("FSAI:");
            sai_middle.ncat(key, sep-key);
            sai_builder->last_group_name = key;
        }
        sai_middle.put(CFG_SEP);
        sai_middle.put('S');
        sai_middle.cat(sep+1);
    }
}

static void nt_build_sai_string(GBDATA *gb_main, const char *topAreaSaiList, GBS_strstruct& topfile, GBS_strstruct& middlefile) {
    // collect all Sais,
    // place some SAI in top area (those listed in 'toparea_SAIs'. SAI-groups will be ignored here)
    // rest of SAI goes into middle area (SAI-groups respected here)

    GBDATA *gb_sai_data = GBT_get_SAI_data(gb_main);
    if (gb_sai_data) {
        GB_HASH *hash = GBS_create_hash(GB_number_of_subentries(gb_sai_data), GB_IGNORE_CASE);

        ConstStrArray topAreaSai;
        GBT_split_string(topAreaSai, topAreaSaiList, ",;: \t", SPLIT_DROPEMPTY);

        for (GBDATA *gb_sai = GBT_first_SAI_rel_SAI_data(gb_sai_data); gb_sai; gb_sai = GBT_next_SAI(gb_sai)) {
            GBDATA *gb_name = GB_search(gb_sai, "name", GB_FIND);
            if (gb_name) {
                char *name = GB_read_string(gb_name);

                bool wantedInTop = false;
                for (unsigned s = 0; !wantedInTop && s<topAreaSai.size(); ++s) {
                    wantedInTop = strcmp(name, topAreaSai[s]) == 0;
                }

                if (!wantedInTop) {
                    GBDATA *gb_gn = GB_search(gb_sai, "sai_group", GB_FIND);
                    char   *gn;

                    if (gb_gn)  gn = GB_read_string(gb_gn);
                    else        gn = ARB_strdup("SAI's");

                    char *cn = new char[strlen(gn) + strlen(name) + 2];
                    sprintf(cn, "%s%c%s", gn, 1, name);
                    GBS_write_hash(hash, cn, 1);
                    delete [] cn;
                    free(gn);
                }
                free(name);
            }
        }

        // add top area SAIs in defined order:
        for (unsigned s = 0; s<topAreaSai.size(); ++s) {
            GBDATA *gb_sai = GBT_find_SAI_rel_SAI_data(gb_sai_data, topAreaSai[s]);
            if (gb_sai) {
                topfile.put(CFG_SEP);
                topfile.put('S');
                topfile.cat(topAreaSai[s]);
            }
        }

        // open surrounding SAI-group:
        middlefile.put(CFG_SEP);
        middlefile.cat("GSAI-Maingroup");

        SAI_string_builder sai_builder = { middlefile, NULp };
        GBS_hash_do_const_sorted_loop(hash, nt_build_sai_string_by_hash, GBS_HCF_sortedByKey, &sai_builder);
        if (sai_builder.last_group_name) {
            middlefile.put(CFG_SEP);
            middlefile.put('E');             // End of old group
        }

        // close surrounding SAI-group:
        middlefile.put(CFG_SEP);
        middlefile.put('E');

        GBS_free_hash(hash);
    }
}

static void nt_build_conf_marked(GBDATA *gb_main, GB_HASH *used, GBS_strstruct& file) {
    file.put(CFG_SEP);
    file.cat("FMore Sequences");

    for (GBDATA *gb_species = GBT_first_marked_species(gb_main);
         gb_species;
         gb_species = GBT_next_marked_species(gb_species))
    {
        char *name = GBT_read_string(gb_species, "name");
        if (!GBS_read_hash(used, name)) {
            file.put(CFG_SEP);
            file.put('L');
            file.cat(name);
        }
        free(name);
    }

    file.put(CFG_SEP);
    file.put('E');   // Group end indicated by 'E'
}

void extract_species_selection(GBDATA *gb_main, const char *selectionName, SelectionExtractType ext_type) {
    GB_transaction  ta(gb_main);
    AW_root        *aw_root = AW_root::SINGLETON;

    if (strcmp(selectionName, NO_CONFIG_SELECTED) == 0) {
        aw_message("Please select a configuration");
    }
    else {
        GB_ERROR   error = NULp;
        GBT_config cfg(gb_main, selectionName, error);

        if (!error) {
            size_t  unknown_species = 0;
            bool    refresh         = false;

            GB_HASH *was_marked = NULp; // only used for SELECTION_COMBINE

            switch (ext_type) {
                case SELECTION_EXTRACT: // unmark all
                    GBT_mark_all(gb_main, 0);
                    refresh = true;
                    break;

                case SELECTION_COMBINE: // store all marked species in hash and unmark them
                    was_marked = GBT_create_marked_species_hash(gb_main);
                    GBT_mark_all(gb_main, 0);
                    refresh    = GBS_hash_elements(was_marked);
                    break;

                default:
                    break;
            }

            for (int area = 0; area<=1 && !error; ++area) {
                GBT_config_parser cparser(cfg, area);

                while (1) {
                    const GBT_config_item& citem = cparser.nextItem(error);
                    if (error || citem.type == CI_END_OF_CONFIG) break;

                    if (citem.type == CI_SPECIES) {
                        GBDATA *gb_species = GBT_find_species(gb_main, citem.name);

                        if (gb_species) {
                            int oldmark = GB_read_flag(gb_species);
                            int newmark = oldmark;
                            switch (ext_type) {
                                case SELECTION_EXTRACT:
                                case SELECTION_MARK:     newmark = 1; break;
                                case SELECTION_UNMARK:   newmark = 0; break;
                                case SELECTION_INVERT:   newmark = !oldmark; break;
                                case SELECTION_COMBINE: {
                                    arb_assert(!oldmark); // should have been unmarked above
                                    newmark = GBS_read_hash(was_marked, citem.name); // mark if was_marked
                                    break;
                                }
                                default: arb_assert(0); break;
                            }
                            if (newmark != oldmark) {
                                GB_write_flag(gb_species, newmark);
                                refresh = true;
                            }
                        }
                        else {
                            unknown_species++;
                        }
                    }
                }
            }

            if (was_marked) GBS_free_hash(was_marked);
            if (unknown_species>0 && !error) error = GBS_global_string("configuration '%s' contains %zu unknown species", selectionName, unknown_species);
            if (refresh) aw_root->awar(AWAR_TREE_REFRESH)->touch();
        }
        aw_message_if(error);
    }
}

static void nt_extract_configuration(UNFIXED, const SelectionAdmin *selection, SelectionExtractType ext_type) {
    GBDATA  *gb_main = selection->get_gb_main();
    AW_root *aw_root = AW_root::SINGLETON;

    char *selectionName = aw_root->awar(selection->get_selection_awarname())->read_string();
    extract_species_selection(gb_main, selectionName, ext_type);
    free(selectionName);
}

static void nt_delete_configuration(AW_window *aww, AW_DB_selection *dbsel, const SelectionAdmin *selection) {
    GBDATA         *gb_main = selection->get_gb_main();
    GB_transaction  ta(gb_main);

    AW_awar *awar_selected    = aww->get_root()->awar(selection->get_selection_awarname());
    char    *name             = awar_selected->read_string();
    GBDATA  *gb_configuration = GBT_find_configuration(gb_main, name);

    if (gb_configuration) {
        dbsel->get_sellist()->move_selection(1);

        GB_ERROR error = GB_delete(gb_configuration);
        error          = ta.close(error);
        if (error) {
            aw_message(error);
        }
        else {
            selection->speciesSelection_deleted_cb(name);
        }
    }
    free(name);
}

static void selected_config_changed_cb(AW_root *root, const SelectionAdmin *selection) {
    const char *config = root->awar(selection->get_selection_awarname())->read_char_pntr();

    bool    nonexisting_config = false;
    GBDATA *gb_target_commment = NULp;
    if (config[0]) {
        GBDATA *gb_configuration = GBT_find_configuration(selection->get_gb_main(), config);
        if (gb_configuration) {
            gb_target_commment = GB_entry(gb_configuration, "comment");
        }
        else {
            nonexisting_config = true;
        }
    }

    AW_awar *awar_comment = root->awar(selection->get_selectionComment_awarname());
    if (gb_target_commment) {
        if (!awar_comment->is_mapped()) awar_comment->write_string("");
        awar_comment->map(gb_target_commment);
    }
    else {
        char *reuse_comment = nonexisting_config ? awar_comment->read_string() : ARB_strdup("");
        if (awar_comment->is_mapped()) {
            awar_comment->unmap();
        }
        awar_comment->write_string(reuse_comment);
        free(reuse_comment);
    }
}
static void config_comment_changed_cb(AW_root *root, const SelectionAdmin *selection) {
    // called when comment-awar changes or gets re-map-ped

    AW_awar    *awar_comment = root->awar(selection->get_selectionComment_awarname());
    const char *comment      = awar_comment->read_char_pntr();

    const char *config           = root->awar(selection->get_selection_awarname())->read_char_pntr();
    GBDATA     *gb_configuration = config[0] ? GBT_find_configuration(selection->get_gb_main(), config) : NULp;

    GB_ERROR error = NULp;
    if (awar_comment->is_mapped()) {
        if (!comment[0]) { // empty existing comment
            arb_assert(gb_configuration);
            GBDATA *gb_commment = GB_entry(gb_configuration, "comment");
            arb_assert(gb_commment);
            if (gb_commment) {
                awar_comment->unmap();
                error = GB_delete(gb_commment);
            }
        }
    }
    else {
        if (comment[0]) { // ignore empty comment for unmapped awar
            if (gb_configuration) {
                arb_assert(!GB_entry(gb_configuration, "comment"));
                error = GBT_write_string(gb_configuration, "comment", comment);
                if (!error) {
                    awar_comment->write_string("");
                    selected_config_changed_cb(root, selection);
                }
            }
            else if (!config[0]) {
                // do NOT warn if name field contains (not yet) existing name
                // (allows to edit comment while creating new config)
                error = "Please select an existing species selection to edit its comment";
            }
        }
    }

    aw_message_if(error);
}
static void init_awars_and_callbacks(AW_root *awr, const SelectionAdmin *selection, bool install_callbacks) {
    AW_awar *awar_selection = awr->awar_string(selection->get_selection_awarname(), DEFAULT_CONFIGURATION, selection->get_gb_main());
    AW_awar *awar_comment   = awr->awar_string(selection->get_selectionComment_awarname(), "", selection->get_gb_main());

    typedef map<AW_awar*, AW_awar*> CorrespondingAwarMap;
    static CorrespondingAwarMap     corresponding;
    typedef CorrespondingAwarMap::iterator CorrespondingIter;

    CorrespondingIter corr_selection = corresponding.find(awar_selection);
    CorrespondingIter corr_comment   = corresponding.find(awar_comment);

    if (corr_selection == corr_comment) {
        arb_assert(corr_selection == corresponding.end()); // otherwise awars are identical (BUG)

        if (install_callbacks) {
            // both are not found -> install callbacks (happens once)
            awar_comment->add_callback(makeRootCallback(config_comment_changed_cb, selection));
            awar_selection->add_callback(makeRootCallback(selected_config_changed_cb, selection))->touch();

            // register both awars as "corresponding":
            corresponding[awar_comment]   = awar_selection;
            corresponding[awar_selection] = awar_comment;
        }
    }
#if defined(ASSERTION_USED)
    else {
        // expect that both are mapped to each other (otherwise comment editing may behave unexpected!)
        arb_assert(corr_selection != corresponding.end() && corr_selection->second == awar_comment);
        arb_assert(corr_comment   != corresponding.end() && corr_comment->second   == awar_selection);
    }
#endif
}

GB_ERROR create_species_selection(const SelectionAdmin& selection, const char *conf_name, int use_species_aside, SelectionCreation creation) {
    GB_ERROR error = NULp;

    if (!conf_name || !conf_name[0]) error = "no config name given";
    else {
        if (use_species_aside==-1) {
            static int last_used_species_aside = 3;
            {
                const char *val                    = GBS_global_string("%i", last_used_species_aside);
                char       *use_species            = aw_input("How many extra species to view aside marked:", val);
                if (use_species) use_species_aside = atoi(use_species);
                free(use_species);
            }

            if (use_species_aside<1) error = "illegal number of 'species aside'";
            else last_used_species_aside = use_species_aside; // remember for next time
        }

        if (!error) {
            AW_root  *awr     = AW_root::SINGLETON;
            GBDATA   *gb_main = selection.get_gb_main();
            TreeNode *tree    = selection.get_tree_root(); // nullable

            init_awars_and_callbacks(awr, &selection, false); // Warning: only creates awars. It's not possible to install callbacks here (selection is an auto variable!)

            GB_transaction ta(gb_main);  // open close transaction

            GBT_config newcfg;
            {
                GB_HASH       *used = GBS_create_hash(GBT_get_species_count(gb_main), GB_MIND_CASE);
                GBS_strstruct  topfile(1000);
                GBS_strstruct  midfile(10000);
                {
                    GBS_strstruct middlefile(10000);
                    nt_build_sai_string(gb_main, selection.get_toparea_SAIs(), topfile, midfile);

                    if (use_species_aside) {
                        Store_species *extra_marked_species = NULp;
                        int            auto_mark            = 0;
                        int            marked_at_right;

                        nt_build_conf_string_rek(used, tree, middlefile, &extra_marked_species, use_species_aside, &auto_mark, use_species_aside, &marked_at_right);
                        if (extra_marked_species) {
                            extra_marked_species->call(unmark_species);
                            delete extra_marked_species;
                        }
                    }
                    else {
                        int dummy_1=0, dummy_2;
                        nt_build_conf_string_rek(used, tree, middlefile, NULp, 0, &dummy_1, 0, &dummy_2);
                    }
                    nt_build_conf_marked(gb_main, used, midfile);
                    midfile.ncat(middlefile.get_data(), middlefile.get_position());
                }

                newcfg.set_definition(GBT_config::TOP_AREA,    topfile.release());
                newcfg.set_definition(GBT_config::MIDDLE_AREA, midfile.release());

                GBS_free_hash(used);
            }

            GBT_config previous(gb_main, conf_name, error);
            error = NULp; // ignore

            const char *prevComment         = NULp; // old or fixed comment
            const char *comment             = NULp;
            bool        warnIfSavingDefault = true;
            switch (creation) {
                case BY_CALLING_THE_EDITOR: { // always saves DEFAULT_CONFIGURATION!
                    prevComment         = "This configuration will be OVERWRITTEN each time\nARB_EDIT4 is started w/o specifying a config!\n---";
                    comment             = "created for ARB_EDIT4";
                    warnIfSavingDefault = false;
                    break;
                }
                case FROM_MANAGER: {
                    if (previous.exists()) {
                        prevComment = previous.get_comment();
                        comment     = "updated manually";
                    }
                    else {
                        prevComment = awr->awar(selection.get_selectionComment_awarname())->read_char_pntr();
                        comment     = "created manually";
                    }
                    break;
                }
                case FROM_IMPORTER: {
                    arb_assert(!previous.exists());
                    comment = "created by importer";
                    break;
                }
            }

            if (prevComment && !prevComment[0]) prevComment = NULp; // handle empty comment like "no comment"

            arb_assert(implicated(prevComment, comment));
            if (comment) {
                // annotate comment with treename
                if (tree) {
                    const char *treename = selection.get_name_of_tree();
                    comment = GBS_global_string("%s (tree=%s)", comment, treename);
                }
                else {
                    comment = GBS_global_string("%s (no tree)", comment);
                }
                char *dated = GBS_log_action_to(prevComment, comment, true);
                newcfg.set_comment(dated);
                free(dated);
            }

            error = newcfg.save(gb_main, conf_name, warnIfSavingDefault);
            awr->awar(selection.get_selection_awarname())->touch(); // refreshes comment field
        }
    }

    return error;
}

static void nt_store_configuration(AW_window*, const SelectionAdmin *selection) {
    const char *cfgName = AW_root::SINGLETON->awar(selection->get_selection_awarname())->read_char_pntr();
    GB_ERROR    err     = create_species_selection(*selection, cfgName, 0, FROM_MANAGER);
    aw_message_if(err);
}

static void nt_rename_configuration(AW_window *aww, const SelectionAdmin *selection) {
    AW_awar *awar_curr_cfg = aww->get_root()->awar(selection->get_selection_awarname());

    char *old_name = awar_curr_cfg->read_string();
    char *new_name = aw_input("Rename selection", "Enter the new name of the selection", old_name);

    if (new_name) {
        GB_ERROR  err     = NULp;
        GBDATA   *gb_main = selection->get_gb_main();

        {
            GB_transaction ta(gb_main);

            GBDATA *gb_existing_cfg  = GBT_find_configuration(gb_main, new_name);
            if (gb_existing_cfg) err = GBS_global_string("There is already a selection named '%s'", new_name);
            else {
                GBDATA *gb_old_cfg = GBT_find_configuration(gb_main, old_name);
                if (gb_old_cfg) {
                    GBDATA *gb_name = GB_entry(gb_old_cfg, "name");
                    if (gb_name) {
                        err = GB_write_string(gb_name, new_name);
                        if (!err) awar_curr_cfg->write_string(new_name);
                    }
                    else err = "Selection has no name";
                }
                else err = "Can't find that selection";
            }
            err = ta.close(err);
        }

        if (err) {
            aw_message(err);
        }
        else {
            arb_assert(GB_get_transaction_level(gb_main) == 0); // otherwise callback below behaves wrong
            selection->speciesSelection_renamed_cb(old_name, new_name);
        }
        free(new_name);
    }
    free(old_name);
}

#pragma GCC diagnostic push
#if (GCC_VERSION_CODE<700)
#pragma GCC diagnostic ignored "-Wstrict-overflow" // gcc 6.x produces a bogus overflow warning (gcc 7.x is smart enough)
#endif

static GB_ERROR swap_configs(GBDATA *gb_main, StrArray& config, int i1, int i2) {
    GB_ERROR error = NULp;

    if (i1>i2) swap(i1, i2); // otherwise overwrite below does not work
    arb_assert(i1<i2 && i1>=0 && i2<int(config.size()));

    GBT_config c1(gb_main, config[i1], error);
    if (!error) {
        GBT_config c2(gb_main, config[i2], error);
        if (!error) error = c1.saveAsOver(gb_main, config[i1], config[i2], false);
        if (!error) error = c2.saveAsOver(gb_main, config[i2], config[i1], false);
        if (!error) config.swap(i1, i2);
    }
    return error;
}

#pragma GCC diagnostic pop

static void reorder_configs_cb(AW_window *aww, awt_reorder_mode mode, AW_DB_selection *sel) {
    AW_root           *awr         = aww->get_root();
    AW_selection_list *sellist     = sel->get_sellist();
    AW_awar           *awar_config = awr->awar(sellist->get_awar_name());
    const char        *selected    = awar_config->read_char_pntr();

    if (selected && selected[0]) {
        int source_idx = sellist->get_index_of(AW_scalar(selected));
        int target_idx = -1;
        switch (mode) {
            case ARM_TOP:    target_idx = 0;            break;
            case ARM_UP:     target_idx = source_idx-1; break;
            case ARM_DOWN:   target_idx = source_idx+1; break;
            case ARM_BOTTOM: target_idx = -1;           break;
        }

        int entries = sellist->size();
        target_idx  = (target_idx+entries)%entries;

        {
            GBDATA         *gb_main = sel->get_gb_main();
            GB_transaction  ta(gb_main);

            StrArray config;
            sellist->to_array(config, true);

            GB_ERROR error = NULp;
            if (source_idx<target_idx) {
                for (int i = source_idx+1; i<=target_idx; ++i) {
                    swap_configs(gb_main, config, i-1, i);
                }
            }
            else if (source_idx>target_idx) {
                for (int i = source_idx-1; i>=target_idx; --i) {
                    swap_configs(gb_main, config, i+1, i);
                }
            }

            error = ta.close(error);
            aw_message_if(error);
        }
        awar_config->touch();
    }
}

static void clear_comment_cb(AW_window *aww, const SelectionAdmin *selection) {
    AW_awar *awar_comment = aww->get_root()->awar(selection->get_selectionComment_awarname());
    char    *comment      = awar_comment->read_string();

    ConstStrArray line;
    GBT_splitNdestroy_string(line, comment, '\n');

    bool    removedDatedLines = false;
    RegExpr datedLine("^([A-Z][a-z]{2}\\s){2}[0-9]+\\s([0-9]{2}:){2}[0-9]{2}\\s[0-9]{4}:\\s", false); // matches lines created with GBS_log_action_to(..., stamp=true)
    for (int i = line.size()-1; i >= 0; --i) {
        const RegMatch *match = datedLine.match(line[i]);
        arb_assert(implicated(!match, !datedLine.has_failed())); // assert RegExpr compiles
        if (match && match->didMatch()) {
            line.safe_remove(i);
            removedDatedLines = true;
        }
    }

    if (!removedDatedLines) line.clear(); // erase all

    comment = GBT_join_strings(line, '\n');
    awar_comment->write_string(comment);
}

static void update_marked_counter_label(GBDATA *gb_species_data, AW_awar *awar_counter_label) {
    /*! Updates marked counter and issues redraw on tree if number of marked species changes.
     * Called on any change of species_information container.
     */
    AW_root    *awr     = AW_root::SINGLETON;
    GBDATA     *gb_main = GB_get_root(gb_species_data);
    long        count   = GBT_count_marked_species(gb_main);
    const char *buffer  = count ? GBS_global_string("%li marked", count) : "";

    if (strcmp(awar_counter_label->read_char_pntr(), buffer)) {
        awar_counter_label->write_string(buffer);
        awr->awar(AWAR_TREE_REFRESH)->touch();
    }
}

void create_species_selection_button(AW_window *awm, WindowCallback wcb, const char *macro_id, const char *awarname_buttontext, GBDATA *gb_main) {
    awm->button_length(13);
    awm->help_text("species_configs.hlp");
    awm->callback(wcb);

    AW_root *awr                = awm->get_root();
    AW_awar *awar_counter_label = awr->awar_string(awarname_buttontext, "unknown", gb_main);

    awm->create_button(macro_id, awarname_buttontext);
    {
        GB_transaction ta(gb_main);

        GBDATA           *gb_species_data = GBT_get_species_data(gb_main);
        DatabaseCallback  dbcb            = makeDatabaseCallback(update_marked_counter_label, awar_counter_label);

        aw_message_if(GB_ensure_callback(gb_species_data, GB_CB_CHANGED, dbcb));
        dbcb(gb_species_data, GB_CB_CHANGED);
    }
}

AW_window *create_species_selection_window(AW_root *root, const SelectionAdmin *selection) {
    init_awars_and_callbacks(root, selection, true);

    AW_window_simple *aws = new AW_window_simple;

    aws->init(root, GBS_global_string("SPECIES_SELECTIONS_%s", selection->get_macro_suffix()), selection->get_window_title());
    aws->load_xfig("nt_selection.fig");

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

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

    aws->at("name");
    aws->create_input_field(selection->get_selection_awarname());

    aws->at("comment");
    aws->create_text_field(selection->get_selectionComment_awarname());

    aws->at("clr");
    aws->callback(makeWindowCallback(clear_comment_cb, selection));
    aws->create_autosize_button("CLEAR", "Clear", "l");

    aws->at("list");
    AW_DB_selection *dbsel = awt_create_CONFIG_selection_list(selection->get_gb_main(), aws, selection->get_selection_awarname());

    aws->button_length(8);

    aws->at("store");
    aws->callback(makeWindowCallback(nt_store_configuration, selection));
    {
        const char *new_id = "STORE";
        aws->create_button(new_id, "STORE", "S");

        // provide intermediate backward compatibility for old, unwanted ID:
        const char *old_id = GBS_global_string("STORE_%s", selection->get_macro_suffix());
        aws->alias_remote_command(old_id, new_id);
    }

    aws->at("extract");
    aws->callback(makeWindowCallback(nt_extract_configuration, selection, SELECTION_EXTRACT));
    aws->create_button("EXTRACT", "EXTRACT", "E");

    aws->at("mark");
    aws->callback(makeWindowCallback(nt_extract_configuration, selection, SELECTION_MARK));
    aws->create_button("MARK", "MARK", "M");

    aws->at("unmark");
    aws->callback(makeWindowCallback(nt_extract_configuration, selection, SELECTION_UNMARK));
    aws->create_button("UNMARK", "UNMARK", "U");

    aws->at("invert");
    aws->callback(makeWindowCallback(nt_extract_configuration, selection, SELECTION_INVERT));
    aws->create_button("INVERT", "INVERT", "I");

    aws->at("combine");
    aws->callback(makeWindowCallback(nt_extract_configuration, selection, SELECTION_COMBINE));
    aws->create_button("COMBINE", "COMBINE", "C");

    aws->at("delete");
    aws->callback(makeWindowCallback(nt_delete_configuration, dbsel, selection));
    aws->create_button("DELETE", "DELETE", "D");

    aws->at("rename");
    aws->callback(makeWindowCallback(nt_rename_configuration, selection));
    aws->create_button("RENAME", "RENAME", "R");

    aws->button_length(0);
    aws->at("sort");
    awt_create_order_buttons(aws, reorder_configs_cb, dbsel);

    return aws;
}

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

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

static bool fold_group(TreeNode *tree, const char *groupName) {
    if (!tree->is_leaf()) {
        if (tree->is_normal_group()) {
            arb_assert(tree->name);
            fprintf(stderr, "group='%s' groupName='%s'\n", tree->name, groupName);
            if (strcmp(tree->name, groupName) == 0) {
                arb_assert(tree->gb_node);
                GBDATA *gb_grouped = GB_entry(tree->gb_node, "grouped");
                if (!gb_grouped) {
                    gb_grouped = GB_create(tree->gb_node, "grouped", GB_BYTE);
                }
                TEST_REJECT_NULL(gb_grouped);
                if (GB_read_byte(gb_grouped) == 0) {
                    TEST_EXPECT_NO_ERROR(GB_write_byte(gb_grouped, 1));
                    return true;
                }
            }
        }
        return
            fold_group(tree->get_leftson(), groupName) ||
            fold_group(tree->get_rightson(), groupName);
    }
    return false;
}

void TEST_build_conf_string() {
    GB_shell shell;
    GBDATA   *gb_main = GB_open("TEST_trees.arb", "r");

    {
        GB_transaction ta(gb_main);

        TreeNode *tree = GBT_read_tree(gb_main, "tree_groups", new SimpleRoot);

        GBS_strstruct  memfile(500);
        Store_species *extra_marked_species = NULp;

        TEST_REJECT_NULL(tree);
        TEST_EXPECT_NO_ERROR(GBT_link_tree(tree, gb_main, false, NULp, NULp));

        int auto_mark = 0;
        int dummy;
        int marked;

        // test result with no species marked:
        GBT_mark_all(gb_main, 0);
        TEST_EXPECT_ZERO(GBT_count_marked_species(gb_main));
        {
            GB_HASH *used = GBS_create_hash(100, GB_IGNORE_CASE);

            memfile.erase();
            marked = nt_build_conf_string_rek(used, tree, memfile, &extra_marked_species, 0, &auto_mark, 0, &dummy);
            TEST_EXPECT_EQUAL(marked, 0);
            TEST_EXPECT_EQUAL(memfile.get_data(), "");
            TEST_EXPECT_NULL(extra_marked_species);

            TEST_EXPECT_EQUAL(GBS_hash_elements(used), 0);
            GBS_free_hash(used);
        }

        // mark some species:
        TEST_EXPECT_NO_ERROR(GBT_restore_marked_species(gb_main, "CloButy2;CloPaste;CorAquat;CurCitre;CloTyro4"));
        TEST_EXPECT_EQUAL(GBT_count_marked_species(gb_main), 5);

        {
            GB_HASH *used = GBS_create_hash(100, GB_IGNORE_CASE);

            memfile.erase();
            marked = nt_build_conf_string_rek(used, tree, memfile, &extra_marked_species, 0, &auto_mark, 0, &dummy);
            TEST_EXPECT_EQUAL(marked, 5); // ------------------------------------- v 'G' indicates the open group
            TEST_EXPECT_EQUAL(memfile.get_data(), "\1Gupper\1LCloButy2\1E\1Glower\1Glow1\1LCurCitre\1LCorAquat\1LCloPaste\1E\1Glow2\1LCloTyro4\1E\1E");
            TEST_EXPECT_NULL(extra_marked_species);

            TEST_EXPECT_EQUAL(GBS_hash_elements(used), 5);
            GBS_free_hash(used);
        }

        // test with closed groups:
        {
            GB_HASH *used = GBS_create_hash(100, GB_IGNORE_CASE);

            TEST_EXPECT(fold_group(tree, "low1"));

            memfile.erase();
            marked = nt_build_conf_string_rek(used, tree, memfile, &extra_marked_species, 0, &auto_mark, 0, &dummy);
            TEST_EXPECT_EQUAL(marked, 5); // ------------------------------------- v 'F' indicates the closed group
            TEST_EXPECT_EQUAL(memfile.get_data(), "\1Gupper\1LCloButy2\1E\1Glower\1Flow1\1LCurCitre\1LCorAquat\1LCloPaste\1E\1Glow2\1LCloTyro4\1E\1E");
            TEST_EXPECT_NULL(extra_marked_species);

            TEST_EXPECT_EQUAL(GBS_hash_elements(used), 5);
            GBS_free_hash(used);
        }

        // test with marking 2 species aside:
        {
            GB_HASH *used = GBS_create_hash(100, GB_IGNORE_CASE);

            memfile.erase();
            marked = nt_build_conf_string_rek(used, tree, memfile, &extra_marked_species, 2, &auto_mark, 0, &dummy);
            TEST_EXPECT_EQUAL(marked, 11);
            TEST_EXPECT_EQUAL(memfile.get_data(), "\1Gupper\1LCloTyro3\1LCloButyr\1LCloButy2\1LCloBifer\1LCloInnoc\1E\1Glower\1Flow1\1LCytAquat\1LCurCitre\1LCorAquat\1LCelBiazo\1LCorGluta\1LCloCarni\1LCloPaste\1E\1Glow2\1Gtwoleafs\1LCloTyrob\1LCloTyro2\1E\1LCloTyro4\1E\1E");

            TEST_REJECT_NULL(extra_marked_species);
            TEST_EXPECT_EQUAL(GBT_count_marked_species(gb_main), 15); // 5 species were marked before + marked 5*2 neighbors
            extra_marked_species->call(unmark_species);
            delete extra_marked_species;
            TEST_EXPECT_EQUAL(GBT_count_marked_species(gb_main), 5);  // 5 previously marked species

            TEST_EXPECT_EQUAL(GBS_hash_elements(used), 15);
            GBS_free_hash(used);
        }

        // test nt_build_conf_marked:
        {
            GB_HASH *used = GBS_create_hash(100, GB_IGNORE_CASE);

            memfile.erase();
            nt_build_conf_marked(gb_main, used, memfile);
            TEST_EXPECT_EQUAL(memfile.get_data(), "\1FMore Sequences\1LCorAquat\1LCurCitre\1LCloButy2\1LCloPaste\1LCloTyro4\1E");

            // exclude 2 species (simulates that they are already in "normal" config)
            GBS_write_hash(used, "CurCitre", 1);
            GBS_write_hash(used, "CloPaste", 1);
            memfile.erase();
            nt_build_conf_marked(gb_main, used, memfile);
            TEST_EXPECT_EQUAL(memfile.get_data(), "\1FMore Sequences\1LCorAquat\1LCloButy2\1LCloTyro4\1E");

            GBS_free_hash(used);
        }

        // test nt_build_sai_string:
        {
            GBS_strstruct topfile(500);

            memfile.erase();
            nt_build_sai_string(gb_main, "HELIX_NR;HELIX;dummy", topfile, memfile);

            TEST_EXPECT_EQUAL(topfile.get_data(), "\1SHELIX_NR\1SHELIX");
            TEST_EXPECT_EQUAL(memfile.get_data(), "\1GSAI-Maingroup\1FSAI:SAI's\1SPOS_VAR_BY_PARSIMONY\1E\1FSAI:special\1SECOLI\1E\1E");
        }

        destroy(tree);
    }

    GB_close(gb_main);
}

#endif // UNIT_TESTS

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


