// ============================================================= //
//                                                               //
//   File      : NT_group_search.cxx                             //
//   Purpose   : GUI for group search                            //
//                                                               //
//   Coded by Ralf Westram (coder@reallysoft.de) in April 2017   //
//   http://www.arb-home.de/                                     //
//                                                               //
// ============================================================= //

#include "NT_group_search.h"
#include "NT_local.h"
#include "ad_trees.h"

#include <group_search.h>
#include <TreeDisplay.hxx>

#include <config_manager.hxx>
#include <sel_boxes.hxx>

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

#include <ad_cb_prot.h>

using namespace std;

#if defined(DEVEL_RALF) && 0
#define TRACE(msg) fprintf(stderr, "TRACE: %s\n", (msg))
#else // !DEVEL_RALF
#define TRACE(msg)
#endif

// --------------------------------
//      AWARs for group search

#define GS_AWARS      "group_search/"
#define GS_AWARS_DUPS GS_AWARS "dup/"
#define GS_AWARS_TMP  "tmp/" GS_AWARS

#define AWAR_MAYBE_INVALID_GROUP   GS_AWARS_TMP "sellist"  // may point to deleted or unlisted group
#define AWAR_SELECTED_RESULT_GROUP GS_AWARS_TMP "selected" // bound to AWAR_MAYBE_INVALID_GROUP, but never points to deleted or unlisted group
#define AWAR_GROUP_HIT_COUNT       GS_AWARS_TMP "hits"
#define AWAR_SELECTED_GROUP_NAME   GS_AWARS_TMP "selname"
#define AWAR_RESULTING_GROUP_NAME  GS_AWARS_TMP "resname"
#define AWAR_TREE_SELECTED         GS_AWARS_TMP "treesel"
#define AWAR_RESULT_ORDER          GS_AWARS_TMP "order"

#define AWAR_SEARCH_WHICH_TREES GS_AWARS "trees"
#define AWAR_SEARCH_MODE        GS_AWARS "mode"
#define AWAR_MATCH_MODE         GS_AWARS "match"
#define AWAR_MARK_TARGET        GS_AWARS "markwhat"
#define AWAR_RENAME_EXPRESSION  GS_AWARS "aci"

#define AWAR_DUPLICATE_MODE       GS_AWARS_DUPS "mode"
#define AWAR_DUP_TREE_MODE        GS_AWARS_DUPS "locmode"
#define AWAR_DUP_NAME_MATCH       GS_AWARS_DUPS "namematch"
#define AWAR_DUP_MIN_CLUSTER_SIZE GS_AWARS_DUPS "clustsize"
#define AWAR_DUP_MIN_WORDS        GS_AWARS_DUPS "minwords"
#define AWAR_DUP_IGNORE_CASE      GS_AWARS_DUPS "ignore_case"
#define AWAR_DUP_EXCLUDED_WORDS   GS_AWARS_DUPS "excluded"
#define AWAR_DUP_WORD_SEPARATORS  GS_AWARS_DUPS "separators"

#define AWARFORMAT_CRIT_OPERATOR GS_AWARS "op%i"
#define AWARFORMAT_CRIT_KEY      GS_AWARS "key%i"
#define AWARFORMAT_CRIT_EQUALS   GS_AWARS "equals%i"
#define AWARFORMAT_CRIT_MATCHES  GS_AWARS "match%i"

#define MAX_CRITERIA 3

inline const char *criterion_awar_name(const char *format, int crit) {
    gs_assert(crit>=1 && crit<=MAX_CRITERIA);
    return GBS_global_string(format, crit);
}

enum TreeSearchRange {
    SEARCH_CURRENT_TREE,
    SEARCH_SELECTED_TREES,
    SEARCH_ALL_TREES,
};

enum DuplicateMode {
    DONT_MIND_DUPLICATES,
    ONLY_DUPLICATES,
    ONLY_UNIQUE,
};

// ---------------------
//      data for UI

class GroupUIdata : virtual Noncopyable {
    GBDATA                *gb_main;
    SmartPtr<GroupSearch>  group_search;
    bool                   show_tree_name; // show treename in result list?
    AW_selection_list     *result_list;
    AW_selection          *tree_list;

    void update_search_filters();
    void update_search_range();
    void update_duplicate_settings();

    void refresh_hit_count(size_t count) {
        AW_root::SINGLETON->awar(AWAR_GROUP_HIT_COUNT)->write_int(count); // update hit count
    }
    void result_order_changed_cb(AW_root *awr) {
        if (group_search.isSet()) {
            GroupSortCriterion crit = GroupSortCriterion(awr->awar(AWAR_RESULT_ORDER)->read_int());
            group_search->addSortCriterion(crit);
            refill_result_list();
        }
    }
    void result_list_awar_changed_cb(AW_root *awr);
    void selected_group_name_changed_cb(AW_root *awr);
    void selected_group_changed_cb(AW_root *awr);
    void update_resulting_groupname_cb(AW_root *awr);

    // callback wrappers:
    static void refresh_result_list_cb(GroupSearch*, GroupUIdata *data) { data->refill_result_list(); }
    static void cleanup_on_exit(GBDATA*, GroupUIdata *data) { data->cleanup(); }
    static void result_order_changed_cb(AW_root *awr, GroupUIdata *data) { data->result_order_changed_cb(awr); }
    static void result_list_awar_changed_cb(AW_root *awr, GroupUIdata *data) { data->result_list_awar_changed_cb(awr); }
    static void selected_group_name_changed_cb(AW_root *awr, GroupUIdata *data) { data->selected_group_name_changed_cb(awr); }
    static void selected_group_changed_cb(AW_root *awr, GroupUIdata *data) { data->selected_group_changed_cb(awr); }
    static void update_resulting_groupname_cb(AW_root *awr, GroupUIdata *data) { data->update_resulting_groupname_cb(awr); }

    void install_callbacks(AW_root *awr) {
        awr->awar(AWAR_RESULT_ORDER)         ->add_callback(makeRootCallback(GroupUIdata::result_order_changed_cb,        this));
        awr->awar(AWAR_MAYBE_INVALID_GROUP)  ->add_callback(makeRootCallback(GroupUIdata::result_list_awar_changed_cb,    this));
        awr->awar(AWAR_SELECTED_GROUP_NAME)  ->add_callback(makeRootCallback(GroupUIdata::selected_group_name_changed_cb, this));
        awr->awar(AWAR_SELECTED_RESULT_GROUP)->add_callback(makeRootCallback(GroupUIdata::selected_group_changed_cb,      this));
        awr->awar(AWAR_RENAME_EXPRESSION)    ->add_callback(makeRootCallback(GroupUIdata::update_resulting_groupname_cb,  this));
    }
    void remove_callbacks(AW_root *awr) {
        awr->awar(AWAR_RESULT_ORDER)         ->remove_callback(makeRootCallback(GroupUIdata::result_order_changed_cb,        this));
        awr->awar(AWAR_MAYBE_INVALID_GROUP)  ->remove_callback(makeRootCallback(GroupUIdata::result_list_awar_changed_cb,    this));
        awr->awar(AWAR_SELECTED_GROUP_NAME)  ->remove_callback(makeRootCallback(GroupUIdata::selected_group_name_changed_cb, this));
        awr->awar(AWAR_SELECTED_RESULT_GROUP)->remove_callback(makeRootCallback(GroupUIdata::selected_group_changed_cb,      this));
        awr->awar(AWAR_RENAME_EXPRESSION)    ->remove_callback(makeRootCallback(GroupUIdata::update_resulting_groupname_cb,  this));
    }

public:
    GroupUIdata(GBDATA *gb_main_) :
        gb_main(gb_main_),
        show_tree_name(false),
        result_list(NULp)
    {
        GB_atclose_callback(gb_main, makeDatabaseCallback(GroupUIdata::cleanup_on_exit, this));
    }

    void initialize() { // called after popup
        if (group_search.isNull()) { // avoid reinit if group-search-menu-entry is pressed while group-search-window is still open
            TRACE("initialize GroupUIdata");
            group_search = new GroupSearch(gb_main, makeGroupSearchCallback(GroupUIdata::refresh_result_list_cb, this));
            install_callbacks(AW_root::SINGLETON);
        }
    }

    GBDATA *get_gb_main() const { return gb_main; }

    void announce_result_list(AW_selection_list *result_list_) { result_list = result_list_; }
    void announce_tree_select_list(AW_selection *tree_list_) { tree_list = tree_list_; }

    void cleanup() { // called on popdown
        TRACE("cleanup GroupUIdata");
        remove_callbacks(AW_root::SINGLETON);
        group_search.setNull();
        clear_result_list();
    }

    void run_search() {
        update_search_filters();
        update_search_range();
        update_duplicate_settings();

        {
            AW_root         *awr  = AW_root::SINGLETON;
            GroupSearchMode  mode = GroupSearchMode(awr->awar(AWAR_SEARCH_MODE)->read_int() ^ awr->awar(AWAR_MATCH_MODE)->read_int());
            group_search->perform_search(mode);
        }
        refill_result_list();
    }

    void refill_result_list();
    void remove_selected_result();
    void remove_all_results();
    void clear_result_list();

    GBDATA *get_selected_group() const {
        AW_scalar awar_value = result_list->get_awar_value();
        int       idx        = result_list->get_index_of(awar_value);

        return idx == -1 ? NULp : awar_value.get_pointer();
    }

    // modify groups
    void delete_selected_group();
    void delete_listed_groups();

    void rename_selected_group();
    void rename_listed_groups();

    void toggle_selected_group_folding();
    void change_listed_groups_folding(GroupFoldingMode mode);

    void mark_species(GroupMarkMode mode);
};

void GroupUIdata::clear_result_list() {
    refresh_hit_count(0);

    result_list->clear();
    result_list->insert_default("<none>", (GBDATA*)NULp);
    result_list->update();
}
void GroupUIdata::refill_result_list() {
    const QueriedGroups& foundGroups = group_search->get_results();

    AW_root *awr      = AW_root::SINGLETON;
    AW_awar *awar_sel = awr->awar(AWAR_SELECTED_RESULT_GROUP);

    if (foundGroups.empty()) {
        clear_result_list();
        awar_sel->write_pointer(NULp);
    }
    else {
        GB_transaction ta(gb_main);

        GBDATA *gb_sellist_group = awr->awar(AWAR_MAYBE_INVALID_GROUP)->read_pointer();
        bool    seen_selected    = false;

        refresh_hit_count(foundGroups.size());

        result_list->clear();
        for (FoundGroupCIter g = foundGroups.begin(); g != foundGroups.end(); ++g) {
            const char *display  = foundGroups.get_group_display(*g, show_tree_name);
            GBDATA     *gb_group = g->get_pointer();

            result_list->insert(display, gb_group);

            if (gb_group == gb_sellist_group) seen_selected = true;
        }
        result_list->insert_default("<none>", (GBDATA*)NULp);
        result_list->update();

        awar_sel->rewrite_pointer(seen_selected ? gb_sellist_group : NULp);
    }
}

void GroupUIdata::update_search_filters() {
    AW_root *awr = AW_root::SINGLETON;

    group_search->forgetQExpressions();
    for (int crit = 1; crit<=MAX_CRITERIA; ++crit) {
        CriterionOperator  op         = crit == 1 ? CO_OR : CriterionOperator(awr->awar(criterion_awar_name(AWARFORMAT_CRIT_OPERATOR, crit))->read_int());
        CriterionType      type       = CriterionType(awr->awar(criterion_awar_name(AWARFORMAT_CRIT_KEY, crit))->read_int());
        CriterionMatch     mtype      = CriterionMatch(awr->awar(criterion_awar_name(AWARFORMAT_CRIT_EQUALS, crit))->read_int());
        const char        *expression = awr->awar(criterion_awar_name(AWARFORMAT_CRIT_MATCHES, crit))->read_char_pntr();

        group_search->addQueryExpression(op, type, mtype, expression);
    }
}

void GroupUIdata::update_search_range() {
    AW_root         *awr   = AW_root::SINGLETON;
    TreeSearchRange  range = TreeSearchRange(awr->awar(AWAR_SEARCH_WHICH_TREES)->read_int());
    TreeNameSet      trees;

    switch (range) {
        case SEARCH_ALL_TREES: break; // empty set means "all trees"
        case SEARCH_CURRENT_TREE: {
            const char *currentTree = awr->awar(AWAR_TREE_NAME)->read_char_pntr();
            if (currentTree[0]) trees.insert(currentTree);
            break;
        }
        case SEARCH_SELECTED_TREES:
            if (tree_list) {
                StrArray tree_names;
                tree_list->get_values(tree_names);
                for (int t = 0; tree_names[t]; ++t) {
                    trees.insert(tree_names[t]);
                }
            }
            break;
    }
    if (trees.empty() && range != SEARCH_ALL_TREES) {
        aw_message("No tree selected -> searching all trees instead");
    }
    group_search->setSearchRange(trees);
    show_tree_name = trees.size() != 1; // (0->all)
}

void GroupUIdata::update_duplicate_settings() {
    AW_root       *awr   = AW_root::SINGLETON;
    DuplicateMode  dmode = DuplicateMode(awr->awar(AWAR_DUPLICATE_MODE)->read_int());

    if (dmode == DONT_MIND_DUPLICATES) {
        group_search->forgetDupCriteria();
    }
    else {
        gs_assert(dmode == ONLY_UNIQUE || dmode == ONLY_DUPLICATES);

        DupTreeCriterionType treetype = DupTreeCriterionType(awr->awar(AWAR_DUP_TREE_MODE)->read_int());
        DupNameCriterionType nametype = DupNameCriterionType(awr->awar(AWAR_DUP_NAME_MATCH)->read_int());

        int     minClusterSize = awr->awar(AWAR_DUP_MIN_CLUSTER_SIZE)->read_int();
        bool    listDups       = dmode == ONLY_DUPLICATES;
        GB_CASE sens           = awr->awar(AWAR_DUP_IGNORE_CASE)->read_int() ? GB_IGNORE_CASE : GB_MIND_CASE;

        if (nametype == DNC_WHOLENAME) {
            group_search->setDupCriteria(listDups, nametype, sens, treetype, minClusterSize);
        }
        else {
            int         minWords       = awr->awar(AWAR_DUP_MIN_WORDS)->read_int();
            const char *excludedWords  = awr->awar(AWAR_DUP_EXCLUDED_WORDS)->read_char_pntr();
            const char *wordSeparators = awr->awar(AWAR_DUP_WORD_SEPARATORS)->read_char_pntr();

            group_search->setDupCriteria(listDups, nametype, sens, minWords, excludedWords, wordSeparators, treetype, minClusterSize);
        }
    }
}


void GroupUIdata::remove_selected_result() {
    int selidx = result_list->get_index_of_selected();
    if (selidx != -1) { // group is selected
        result_list->move_selection(1);
        result_list->delete_element_at(selidx);
        result_list->update();
        group_search->remove_hit(selidx);
    }
}
void GroupUIdata::remove_all_results() {
    group_search->forget_results();
    refill_result_list();
}

void GroupUIdata::delete_selected_group() {
    int sel = result_list->get_index_of_selected();
    if (sel != -1) { // group is selected
        result_list->select_default(); // avoid invalid access to group deleted below
        aw_message_if(group_search->delete_group(sel));
    }
}

void GroupUIdata::delete_listed_groups() {
    if (group_search->has_results()) {
        result_list->select_default(); // avoid invalid access to group deleted below
        aw_message_if(group_search->delete_found_groups());
    }
}

// callback wrappers:
static void popdown_search_window_cb(AW_window*, GroupUIdata *data) { data->cleanup(); }
static void runGroupSearch_cb(AW_window*, GroupUIdata *data) { data->run_search(); }
static void remove_hit_cb(AW_window*, GroupUIdata *data) { data->remove_selected_result(); }
static void clear_results_cb(AW_window*, GroupUIdata *data) { data->remove_all_results(); }
static void delete_selected_group_cb(AW_window*, GroupUIdata *data) { data->delete_selected_group(); }
static void delete_listed_groups_cb(AW_window*, GroupUIdata *data) { data->delete_listed_groups(); }
static void rename_selected_group_cb(AW_window*, GroupUIdata *data) { data->rename_selected_group(); }
static void rename_listed_groups_cb(AW_window*, GroupUIdata *data) { data->rename_listed_groups(); }
static void double_click_group_cb(AW_window*, GroupUIdata *data) { data->toggle_selected_group_folding(); }
static void listed_groups_folding_cb(AW_window*, GroupUIdata *data, GroupFoldingMode mode) { data->change_listed_groups_folding(mode); }
static void group_mark_cb(AW_window*, GroupUIdata *data, GroupMarkMode mode) { data->mark_species(mode); }


TREE_canvas *NT_get_canvas_showing_tree(const char *tree_name, bool forceDisplay) {
    // search whether any canvas shows the tree 'tree_name'.
    // if yes -> return that canvas
    // if no -> if forceDisplay==false -> return NULp
    //          else -> search first visible canvas + switch tree -> return that canvas

    TREE_canvas *ntw = NULp;
    {
        TREE_canvas *first_vis = NULp;

        for (int ci = 0; ci<MAX_NT_WINDOWS && !ntw; ++ci) {
            TREE_canvas *tc = NT_get_canvas_by_index(ci);
            if (tc && tc->is_shown()) { // @@@ does not detect iconified windows
                if (!first_vis) first_vis = tc;
                if (strcmp(tc->get_awar_tree()->read_char_pntr(), tree_name) == 0) ntw = tc;
            }
        }

        if (!ntw && forceDisplay) {
            if (!first_vis) {
                first_vis = NT_get_canvas_by_index(0);
                first_vis->aww->activate(); // popup first (if all windows hidden)
            }
            ntw = first_vis;
            nt_assert(ntw);
            ntw->get_awar_tree()->write_string(tree_name);
        }
    }

    return ntw;
}

static TREE_canvas *get_canvas_able_to_show(GBDATA *gb_group) {
    // detect tree containing group
    // + use NT_get_canvas_showing_tree to force display of that tree

    TREE_canvas *ntw = NULp;
    {
        GB_transaction ta(gb_group);

        GBDATA *gb_tree   = GB_get_father(gb_group);
        char   *tree_name = GB_read_key(gb_tree);

        ntw = NT_get_canvas_showing_tree(tree_name, true); // forces display of 'tree_name'

        free(tree_name);
    }

    return ntw;
}

static bool inside_group_selection = false;

void GroupUIdata::result_list_awar_changed_cb(AW_root *awr) {
    LocallyModify<bool> avoid_recursion(inside_group_selection, true);

    GBDATA *gb_group = awr->awar(AWAR_MAYBE_INVALID_GROUP)->read_pointer();
    TRACE(GBS_global_string("result_list_awar_changed_cb to %p", gb_group));

    if (gb_group) {
        TREE_canvas *ntw = get_canvas_able_to_show(gb_group);

        GB_transaction  ta(gb_group);
        ntw->get_graphic_tree()->select_group(gb_group);

        awr->awar(AWAR_SELECTED_RESULT_GROUP)->rewrite_pointer(get_selected_group());
    }
    else {
        // @@@ why need extra refresh here?
        // @@@ does not auto-fold
        // both should be handled by deselect_group?!
        TREE_canvas      *ntw = NT_get_canvas_by_index(0); // use any canvas here
        AWT_auto_refresh  force(ntw);
        ntw->get_graphic_tree()->deselect_group();
        ntw->request_refresh();

        awr->awar(AWAR_SELECTED_RESULT_GROUP)->rewrite_pointer(NULp);
    }
}

static void selected_group_changed_by_canvas_cb(AW_root *awr) {
    if (!inside_group_selection) {
        LocallyModify<bool> avoid_recursion(inside_group_selection, true);

        GBDATA *gb_group = awr->awar(AWAR_GROUP)->read_pointer();
        TRACE(GBS_global_string("selected_group_changed_by_canvas_cb to %p", gb_group));
        if (gb_group) {
            awr->awar(AWAR_MAYBE_INVALID_GROUP)->write_pointer(gb_group);
        }
    }
}

static bool nameChangedByGroupChange = false;

void GroupUIdata::selected_group_changed_cb(AW_root *awr) {
    GBDATA *gb_group = awr->awar(AWAR_SELECTED_RESULT_GROUP)->read_pointer();
    TRACE(GBS_global_string("selected_group_changed_cb to %p", gb_group));

    LocallyModify<bool> ignoreNameChange(nameChangedByGroupChange, true);

    const char *name = "";
    if (gb_group) {
        GB_transaction ta(gb_group);
        name = FoundGroup(gb_group).get_name();
    }
    awr->awar(AWAR_SELECTED_GROUP_NAME)->write_string(name);
    // ensure update of rename-result even if a group-change did NOT change the name (because both groups have same name)
    update_resulting_groupname_cb(awr);
}

void GroupUIdata::update_resulting_groupname_cb(AW_root *awr) {
    TRACE("update_resulting_groupname_cb");

    char *curr_name = awr->awar(AWAR_SELECTED_GROUP_NAME)->read_string();
    char *acisrt    = awr->awar(AWAR_RENAME_EXPRESSION)->read_string();

    ARB_ERROR  error;
    int        idx    = result_list->get_index_of_selected();
    char      *result = GS_calc_resulting_groupname(gb_main, group_search->get_results(), idx, curr_name, acisrt, error);

    if (result) {
        error.expect_no_error();
        if (!result[0]) { // empty result (will be skipped in batch-rename)
            result = strdup("<empty result>  =>  group would be skipped in batch-rename");
        }
    }
    else {
        result = strdup(error.deliver()); // show error in result field
    }
    awr->awar(AWAR_RESULTING_GROUP_NAME)->write_string(result);

    free(result);
    free(acisrt);
    free(curr_name);
}

void GroupUIdata::selected_group_name_changed_cb(AW_root *awr) {
    if (!nameChangedByGroupChange) {
        GB_ERROR  error                = NULp;
        AW_awar  *awar_selected_result = awr->awar(AWAR_SELECTED_RESULT_GROUP);
        GBDATA   *gb_group             = awar_selected_result->read_pointer();

        if (!gb_group) error = "select a group to rename it";
        else {
            char *new_name          = GBS_trim(awr->awar(AWAR_SELECTED_GROUP_NAME)->read_char_pntr());
            if (!new_name[0]) error = "empty group name not allowed";
            else {
                GB_transaction ta(gb_group);
                error = GBT_write_name_to_groupData(gb_group, false, new_name, true);
            }
        }

        if (error) {
            aw_message(error);
            awar_selected_result->touch(); // refill groupname inputfield
        }
    }
}

void GroupUIdata::rename_selected_group() {
    int sel = result_list->get_index_of_selected();
    if (sel != -1) { // group is selected
        const char *acisrt = AW_root::SINGLETON->awar(AWAR_RENAME_EXPRESSION)->read_char_pntr();
        aw_message_if(group_search->rename_group(sel, acisrt).deliver());
        // Note: no refresh needed here (triggered by taxonomy callbacks)
    }
}

void GroupUIdata::rename_listed_groups() {
    if (group_search.isNull()) {
        aw_message("Please rerun group search");
        return;
    }
    if (group_search->has_results()) {
        const char *acisrt = AW_root::SINGLETON->awar(AWAR_RENAME_EXPRESSION)->read_char_pntr();
        aw_message_if(group_search->rename_found_groups(acisrt).deliver());
        // Note: no refresh needed here (triggered by taxonomy callbacks)
    }
}

void GroupUIdata::toggle_selected_group_folding() {
    int sel = result_list->get_index_of_selected();
    if (sel != -1) { // group is selected
        ARB_ERROR error = group_search->fold_group(sel, GFM_TOGGLE);
        aw_message_if(error);
        if (!error) AW_root::SINGLETON->awar(AWAR_GROUP)->touch(); // trigger recenter + refresh of changed group
    }
}

void GroupUIdata::change_listed_groups_folding(GroupFoldingMode foldmode) {
    if (group_search->has_results() || (foldmode & GFM_COLLAPSE_REST)) {
        ARB_ERROR error = group_search->fold_found_groups(foldmode);
        aw_message_if(error);
        if (!error) {
            AW_root::SINGLETON->awar(AWAR_GROUP)->write_pointer(NULp); // deselect group (otherwise wont fold subtree containing selected)
            AW_root::SINGLETON->awar(AWAR_TREE_REFRESH)->touch(); // force expose of all trees (triggers reload if DB-changes)
        }
    }
}

enum GroupMarkTarget {
    GMT_ALL_SPECIES,    // targets all species in DB
    GMT_SELECTED_GROUP, // targets all species contained in SELECTED group
    GMT_ANY_GROUP,      // targets all species contained in ANY listed group
    GMT_ALL_GROUPS,     // targets all species contained in ALL listed groups
};

void GroupUIdata::mark_species(GroupMarkMode mode) {
    GroupMarkTarget target   = GroupMarkTarget(AW_root::SINGLETON->awar(AWAR_MARK_TARGET)->read_int());
    bool            anyGroup = false;
    ARB_ERROR       error;
    GB_transaction  ta(gb_main);

    switch (target) {
        case GMT_ALL_SPECIES:
            GBT_mark_all(gb_main, int(mode));
            break;

        case GMT_SELECTED_GROUP: {
            int sel = result_list->get_index_of_selected();
            if (sel != -1) {
                error = group_search->set_marks_in_group(sel, mode);
            }
            else {
                error = "Please select a group";
            }

            break;
        }
        case GMT_ANY_GROUP:
            anyGroup = true;
            // fall-through
        case GMT_ALL_GROUPS:
            if (group_search->has_results()) {
                error = group_search->set_marks_in_found_groups(mode, anyGroup ? UNITE : INTERSECT);
            }
            else {
                error = "No results listed";
            }
            break;
    }

    error = ta.close(error);
    aw_message_if(error.deliver());
}

void create_group_search_awars(AW_root *aw_root, AW_default props) {
    aw_root->awar_pointer(AWAR_MAYBE_INVALID_GROUP, NULp, props);
    aw_root->awar_pointer(AWAR_SELECTED_RESULT_GROUP, NULp, props);

    CriterionType deftype[MAX_CRITERIA] = {
        CT_NAME,
        CT_SIZE,
        CT_MARKED_PC,
    };

    for (int crit = 1; crit<=MAX_CRITERIA; ++crit) {
        if (crit>1) {
            aw_root->awar_int(criterion_awar_name(AWARFORMAT_CRIT_OPERATOR, crit), CO_IGNORE, props);
        }
        aw_root->awar_int   (criterion_awar_name(AWARFORMAT_CRIT_KEY,  crit), deftype[crit-1],      props);
        aw_root->awar_int   (criterion_awar_name(AWARFORMAT_CRIT_EQUALS,  crit), CM_MATCH,             props);
        aw_root->awar_string(criterion_awar_name(AWARFORMAT_CRIT_MATCHES, crit), crit == 1 ? "*" : "", props);
    }

    aw_root->awar_string(AWAR_SELECTED_GROUP_NAME,  "",    props);
    aw_root->awar_string(AWAR_RENAME_EXPRESSION,    "",    props);
    aw_root->awar_string(AWAR_RESULTING_GROUP_NAME, "???", props);
    aw_root->awar_string(AWAR_TREE_SELECTED,        "",    props);

    aw_root->awar_string(AWAR_DUP_EXCLUDED_WORDS,
                         "and or"
                         " branch group clade subsection"
                         " order family genus"
                         " incertae sedis"
                         " other unknown unclassified miscellaneous"
                         , props);
    aw_root->awar_string(AWAR_DUP_WORD_SEPARATORS, ",; /()[]_", props);

    aw_root->awar_int(AWAR_GROUP_HIT_COUNT,      0,                    props);
    aw_root->awar_int(AWAR_SEARCH_WHICH_TREES,   SEARCH_CURRENT_TREE,  props);
    aw_root->awar_int(AWAR_SEARCH_MODE,          GSM_FIND,             props);
    aw_root->awar_int(AWAR_MATCH_MODE,           GSM_MATCH,            props);
    aw_root->awar_int(AWAR_DUPLICATE_MODE,       DONT_MIND_DUPLICATES, props);
    aw_root->awar_int(AWAR_DUP_TREE_MODE,        DLC_SAME_TREE,        props);
    aw_root->awar_int(AWAR_DUP_MIN_CLUSTER_SIZE, 2,                    props)->set_min(2);
    aw_root->awar_int(AWAR_DUP_NAME_MATCH,       DNC_WHOLENAME,        props);
    aw_root->awar_int(AWAR_DUP_IGNORE_CASE,      1,                    props);
    aw_root->awar_int(AWAR_DUP_MIN_WORDS,        2,                    props)->set_min(1);
    aw_root->awar_int(AWAR_RESULT_ORDER,         GSC_NONE,             props);
    aw_root->awar_int(AWAR_MARK_TARGET,          GMT_SELECTED_GROUP,   props);

    // perma-callbacks:
    aw_root->awar(AWAR_GROUP)  ->add_callback(selected_group_changed_by_canvas_cb);
    // more callbacks are installed above in .@install_callbacks
}

static AW_window *create_tree_select_window_cb(AW_root *aw_root, GroupUIdata *data) {
    AW_window_simple *aws = new AW_window_simple;

    aw_root->awar(AWAR_SEARCH_WHICH_TREES)->write_int(SEARCH_SELECTED_TREES); // switch target-range to 'selected trees'

    aws->init(aw_root, "GROUP_TREES", "Select trees to search");
    aws->load_xfig("group_trees.fig");

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

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

    aws->at("list");
    AW_DB_selection *all_trees      = awt_create_TREE_selection_list(data->get_gb_main(), aws, AWAR_TREE_SELECTED);
    AW_selection    *selected_trees = awt_create_subset_selection_list(aws, all_trees->get_sellist(), "selected", "add", "sort");

    data->announce_tree_select_list(selected_trees);

    return aws;
}

static AW_window *create_group_rename_window_cb(AW_root *awr, GroupUIdata *data) {
    AW_window_simple *aws = new AW_window_simple;
    aws->init(awr, "RENAME_GROUPS", "Rename taxonomic groups");

    const int PAD = 10;

    aws->at(PAD, PAD);
    aws->auto_space(PAD/2, PAD/2);

    aws->button_length(7);

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

    aws->at_shift(450, 0); // defines minimum window width

    aws->at_attach(-PAD, 0);
    aws->callback(makeHelpCallback("group_rename.hlp"));
    aws->create_button("HELP", "Help", "H");
    aws->at_unattach();

    const int IF_YSIZE = 32;       // lineheight of attached input field

    const int LABEL_LENGTH = 27;
    const int FIELD_LENGTH = 40;  // startup-length (chars)

    aws->label_length(LABEL_LENGTH);

    aws->at_newline();
    int sy = aws->get_at_yposition();

    aws->at_attach_to(true, false, -PAD, IF_YSIZE);
    aws->label("Selected group name:");
    aws->create_input_field(AWAR_SELECTED_GROUP_NAME, FIELD_LENGTH);

    aws->at_newline();
    int inputlineHeight = aws->get_at_yposition()-sy;

    aws->at_attach_to(true, false, -PAD, IF_YSIZE);
    aws->label("Modify using ACI/SRT:");
    aws->create_input_field(AWAR_RENAME_EXPRESSION, FIELD_LENGTH);
    aws->at_unattach();

    aws->at_newline();
    int rx, ry;
    aws->get_at_position(&rx, &ry);
    aws->at_shift(0, inputlineHeight); // reserve space for "Resulting group name"

    int by = aws->get_at_yposition();
    aws->at_attach(0, -PAD);
    aws->callback(makeWindowCallback(rename_selected_group_cb, data));
    aws->create_autosize_button("SELECTED", "Apply to\nselected group");
    aws->at_attach(0, -PAD);
    aws->callback(makeWindowCallback(rename_listed_groups_cb, data));
    aws->create_autosize_button("LISTED", "Apply to all\nlisted groups");
    aws->at_unattach();

    aws->at_newline();
    int bheight = aws->get_at_yposition()-by;

    aws->at(rx, ry);
    aws->at_attach_to(true, true, -PAD, -(PAD+bheight));
    aws->label("Resulting group name:");
    aws->create_button(NULp, AWAR_RESULTING_GROUP_NAME, NULp, "+");
    aws->at_unattach();

    aws->window_fit();

    return aws;
}

static AW_window *create_dup_config_window_cb(AW_root *awr) {
    AW_window_simple *aws = new AW_window_simple;
    aws->init(awr, "DUP_CONFIG", "Configure group duplicate search");

    const int PAD = 10;
    const int IF_YSIZE = 32;       // lineheight of attached input field

    aws->at(PAD, PAD);
    aws->auto_space(PAD/2, PAD/2);

    aws->button_length(7);

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

    const int LONG_LABEL_LENGTH  = 30;
    const int SHORT_LABEL_LENGTH = 15;
    const int LONG_FIELD_LENGTH  = 40; // used for string input field
    const int SHORT_FIELD_LENGTH = 7;  // used for numeric input fields

    aws->label_length(LONG_LABEL_LENGTH);

    aws->at_newline();
    aws->label("Min. size of duplicate cluster:");
    aws->create_input_field(AWAR_DUP_MIN_CLUSTER_SIZE, SHORT_FIELD_LENGTH);

    aws->at_newline();
    aws->label("Search duplicates");
    aws->create_option_menu(AWAR_DUP_TREE_MODE);
    aws->insert_default_option("inside same tree",   "s", DLC_SAME_TREE);
    aws->insert_option        ("in different trees", "d", DLC_DIFF_TREE);
    aws->insert_option        ("anywhere",           "a", DLC_ANYWHERE);
    aws->update_option_menu();

    aws->at_newline();
    aws->label("Ignore case?");
    aws->create_toggle(AWAR_DUP_IGNORE_CASE);

    aws->at_newline();
    aws->create_toggle_field(AWAR_DUP_NAME_MATCH, "Duplicates are names that");
    aws->insert_default_toggle("match whole name", "n", DNC_WHOLENAME);
    aws->insert_toggle        ("match wordwise",   "w", DNC_WORDWISE);
    aws->update_toggle_field();

    aws->at_newline();
    aws->label("Min. number of matching words");
    aws->create_input_field(AWAR_DUP_MIN_WORDS, SHORT_FIELD_LENGTH);

    aws->at_newline();
    aws->label("Word separators");
    aws->create_input_field(AWAR_DUP_WORD_SEPARATORS, 2*SHORT_FIELD_LENGTH);

    aws->at_newline();
    aws->label_length(SHORT_LABEL_LENGTH);
    aws->at_attach_to(true, false, -PAD, IF_YSIZE);
    aws->label("Ignored words");
    aws->create_input_field(AWAR_DUP_EXCLUDED_WORDS, LONG_FIELD_LENGTH);

    aws->window_fit();
    return aws;
}

static AWT_config_mapping_def group_search_config_mapping[] = {
    { AWAR_SEARCH_WHICH_TREES, "searched_trees" },
    { AWAR_SEARCH_MODE,        "search_mode" },
    { AWAR_MATCH_MODE,         "match_mode" },

    { AWAR_DUPLICATE_MODE,       "dup_mode" },
    { AWAR_DUP_TREE_MODE,        "dup_tree_mode" },
    { AWAR_DUP_MIN_CLUSTER_SIZE, "dup_min_size" },
    { AWAR_DUP_NAME_MATCH,       "dup_match_mode" },
    { AWAR_DUP_IGNORE_CASE,      "dup_ignore_case" },
    { AWAR_DUP_MIN_WORDS,        "dup_min_words" },
    { AWAR_DUP_EXCLUDED_WORDS,   "dup_excluded_words" },
    { AWAR_DUP_WORD_SEPARATORS,  "dup_word_separators" },

    { AWAR_MARK_TARGET,       "mark_target" },
    { AWAR_RENAME_EXPRESSION, "rename_script" },

    { NULp, NULp },
};

static void create_search_config_setup_cb(AWT_config_definition& def) {
    def.add(group_search_config_mapping); // fixed parameters

    for (int crit = 1; crit<=MAX_CRITERIA; ++crit) {
        if (crit>1) {
            def.add(criterion_awar_name(AWARFORMAT_CRIT_OPERATOR, crit), "op", crit);
        }
        def.add(criterion_awar_name(AWARFORMAT_CRIT_KEY, crit), "sel", crit);
        def.add(criterion_awar_name(AWARFORMAT_CRIT_EQUALS, crit), "eq", crit);
        def.add(criterion_awar_name(AWARFORMAT_CRIT_MATCHES, crit), "expr", crit);
    }
}

static AWT_predefined_config predefined_group_search[] = {
    { "*tagged_find",           "Search expression for \"tagged\" groupnames",                                                         "dup_mode='0';eq1='0';expr1='*[*]*';match_mode='0';op2='2';op3='2';sel1='0'" },
    { "*tags_remove_all",       "Expression for batch-rename:\n- removes any prefix(es) in \"[..]\" from groupnames",                  "rename_script='/\\\\[.*\\\\]//'" },
    { "*tag_prefix",            "Expression for batch-rename:\n- adds prefix \"[TAG] \" to groupnames",                                "rename_script='\"[TAG] \";dd'" },
    { "*swap_AND_names",        "Batch-rename:\n \"X and Y\"    -> \"Y and X\"\n \"X, Y and Z\" -> \"Z, X and Y\" ...",                "rename_script=':* and *=*2 and *1:* and *, *=*1, *2 and *3:*, * and *, *=*1, *2, *3 and *4'" },
    { "*rename_enumerated",     "Batch-rename:\n- appends running number to group-name\n  (using duplicate search + hitlist order)",   "dup_mode='1';rename_script='dd;\"~\";dupidx|merge|srt(~~=~)'" },
    { "*remove_numeric_suffix", "Batch-rename:\n- removes numeric suffixes \n  like \"~1\", \"~003\"",                                 "rename_script='/~[0-9]+$//'" },

    { "*undo_groupXfer_report", "undo group rename applied by \"Move groups\":\n * remove prefix \"XFRD_\"\n * remove penalty suffix", "rename_script='command(\"/^XFRD_//\")|command(\"/\\\\\\\\{penalty.*\\\\\\\\}$//\")'" },

    { NULp, NULp, NULp }
};

void popup_group_search_window(AW_window *aw_parent, GBDATA *gb_main) {
    static AW_window   *awgs = NULp;
    static GroupUIdata  data(gb_main);

    data.initialize();

    if (!awgs) {
        AW_window_simple *aws     = new AW_window_simple;
        AW_root          *aw_root = aw_parent->get_root();

        aws->init(aw_root, "GROUP_SEARCH", "Search taxonomic groups");
        aws->load_xfig("group_search.fig");

        aws->button_length(7);

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

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

        aws->at("config");
        AWT_insert_config_manager(aws, AW_ROOT_DEFAULT, "group_search", makeConfigSetupCallback(create_search_config_setup_cb), NULp, predefined_group_search);

        aws->at("trees");
        aws->create_option_menu(AWAR_SEARCH_WHICH_TREES);
        aws->insert_default_option("current tree",   "c", SEARCH_CURRENT_TREE);
        aws->insert_option        ("selected trees", "s", SEARCH_SELECTED_TREES);
        aws->insert_option        ("all trees",      "a", SEARCH_ALL_TREES);
        aws->update_option_menu();

        aws->at("select");
        aws->callback(makeCreateWindowCallback(create_tree_select_window_cb, &data));
        aws->create_autosize_button("select_trees", "(select)");

        aws->at("mode");
        aws->create_option_menu(AWAR_SEARCH_MODE);
        aws->insert_default_option("list",   "l", GSM_FIND);
        aws->insert_option        ("add",    "a", GSM_ADD);
        aws->insert_option        ("keep",   "k", GSM_KEEP);
        aws->insert_option        ("remove", "r", GSM_REMOVE);
        aws->update_option_menu();

        aws->at("not");
        aws->create_option_menu(AWAR_MATCH_MODE);
        aws->insert_default_option("match",       "m", GSM_MATCH);
        aws->insert_option        ("don't match", "d", GSM_MISMATCH);
        aws->update_option_menu();

        aws->at("dups");
        aws->create_option_menu(AWAR_DUPLICATE_MODE);
        aws->insert_default_option("No",                    "n", DONT_MIND_DUPLICATES);
        aws->insert_option        ("duplicate groups only", "d", ONLY_DUPLICATES);
        aws->insert_option        ("unique groups only",    "u", ONLY_UNIQUE);
        aws->update_option_menu();

        aws->at("dupconf");
        aws->callback(create_dup_config_window_cb);
        aws->create_autosize_button("config_dup", "Configure");

        WindowCallback search_wcb = makeWindowCallback(runGroupSearch_cb, &data);

        for (int crit = 1; crit<=MAX_CRITERIA; ++crit) {
            if (crit>1) {
                aws->at(GBS_global_string("op%i", crit));
                aws->create_option_menu(criterion_awar_name(AWARFORMAT_CRIT_OPERATOR, crit));
                aws->insert_option        ("and", "a", CO_AND);
                aws->insert_option        ("or",  "o", CO_OR);
                aws->insert_default_option("ign", "i", CO_IGNORE);
                aws->update_option_menu();
            }

            aws->at(GBS_global_string("crit%i", crit));
            aws->create_option_menu(criterion_awar_name(AWARFORMAT_CRIT_KEY, crit));
            aws->insert_default_option("groupname",    "g", CT_NAME);
            aws->insert_option        ("parent",       "p", CT_PARENT_DIRECT);
            aws->insert_option        ("parent (any)", "a", CT_PARENT_ANY);
            aws->insert_option        ("parent (all)", "l", CT_PARENT_ALL);
            aws->insert_option        ("nesting",      "n", CT_NESTING_LEVEL);
            aws->insert_option        ("folded",       "f", CT_FOLDED);
            aws->insert_option        ("size",         "s", CT_SIZE);
            aws->insert_option        ("marked",       "m", CT_MARKED);
            aws->insert_option        ("marked%",      "%", CT_MARKED_PC);
            aws->insert_option        ("zombies",      "z", CT_ZOMBIES);
            aws->insert_option        ("AID",          "A", CT_AID);
            aws->insert_option        ("keeled",       "k", CT_KEELED);
            aws->update_option_menu();

            aws->at(GBS_global_string("eq%i", crit));
            aws->create_toggle(criterion_awar_name(AWARFORMAT_CRIT_EQUALS, crit), "#equal.xpm", "#notEqual.xpm");

            aws->at(GBS_global_string("content%i", crit));
            aws->d_callback(search_wcb); // ENTER in search field
            aws->create_input_field(criterion_awar_name(AWARFORMAT_CRIT_MATCHES, crit));
        }

        aws->button_length(18);

        aws->at("doquery");
        aws->callback(search_wcb);
        aws->create_button("SEARCH", "Search", "S", "+");

        aws->button_length(13);

        aws->at("count");
        aws->label("Hits:");
        aws->create_button(NULp, AWAR_GROUP_HIT_COUNT, NULp, "+");

        aws->at("result");
        aws->d_callback(makeWindowCallback(double_click_group_cb, &data));
        AW_selection_list *result_list = aws->create_selection_list(AWAR_MAYBE_INVALID_GROUP);
        data.announce_result_list(result_list);
        data.clear_result_list();

        aws->at("order");
        aws->create_option_menu(AWAR_RESULT_ORDER);
        aws->insert_default_option("unsorted",     "u", GSC_NONE);
        aws->insert_option        ("by name",      "n", GSC_NAME);
        aws->insert_option        ("by nesting",   "g", GSC_NESTING);
        aws->insert_option        ("by size",      "s", GSC_SIZE);
        aws->insert_option        ("by marked",    "m", GSC_MARKED);
        aws->insert_option        ("by marked%",   "%", GSC_MARKED_PC);
        aws->insert_option        ("by treename",  "t", GSC_TREENAME);
        aws->insert_option        ("by treeorder", "o", GSC_TREEORDER);
        aws->insert_option        ("by hit",       "h", GSC_HIT_REASON);
        aws->insert_option        ("by cluster",   "c", GSC_CLUSTER);
        aws->insert_option        ("by AID",       "A", GSC_AID);
        aws->insert_option        ("by keeled",    "k", GSC_KEELED);
        aws->insert_option        ("reverse",      "r", GSC_REVERSE);
        aws->update_option_menu();

        // actions (results):
        aws->button_length(6);

        aws->at("remhit");
        aws->callback(makeWindowCallback(remove_hit_cb, &data));
        aws->create_button("rm_sel", "Remove");

        aws->at("clear");
        aws->callback(makeWindowCallback(clear_results_cb, &data));
        aws->create_button("clear", "Clear");

        // actions (groups):
        // ..... rename
        aws->at("rename");
        aws->callback(makeCreateWindowCallback(create_group_rename_window_cb, &data));
        aws->create_button("rename", "Rename ...");

        // ..... expand/collapse
        aws->at("expand");
        aws->callback(makeWindowCallback(listed_groups_folding_cb, &data, GFM_EXPANDREC));
        aws->create_button("expand_listed", "Expand listed");

        aws->at("expcol");
        aws->callback(makeWindowCallback(listed_groups_folding_cb, &data, GFM_EXPANDREC_COLLREST));
        aws->create_button("explst_collrest", "Expand listed\ncollapse rest");

        aws->at("expparent");
        aws->callback(makeWindowCallback(listed_groups_folding_cb, &data, GFM_EXPANDPARENTS));
        aws->create_button("expand_parents", "Expand parents");

        aws->at("collapse");
        aws->callback(makeWindowCallback(listed_groups_folding_cb, &data, GFM_COLLAPSE));
        aws->create_button("collapse_listed", "Collapse listed");

        // ..... delete
        aws->at("delete");
        aws->callback(makeWindowCallback(delete_selected_group_cb, &data));
        aws->create_button("del_sel", "Destroy\nselected group");

        aws->at("dellisted");
        aws->callback(makeWindowCallback(delete_listed_groups_cb, &data));
        aws->create_button("del_listed", "Destroy all\nlisted groups");

        // ..... mark/unmark
        aws->at("mark");
        aws->callback(makeWindowCallback(group_mark_cb, &data, GMM_MARK));
        aws->create_button("mark", "Mark");
        aws->at("unmark");
        aws->callback(makeWindowCallback(group_mark_cb, &data, GMM_UNMARK));
        aws->create_button("unmark", "Unmark");
        aws->at("inv");
        aws->callback(makeWindowCallback(group_mark_cb, &data, GMM_INVERT));
        aws->create_button("invert", "Inv");

        aws->at("mwhich"); // needs to be created AFTER delete buttons
        aws->create_option_menu(AWAR_MARK_TARGET);
        aws->insert_default_option("selected",   "s", GMT_SELECTED_GROUP);
        aws->insert_option        ("any listed", "l", GMT_ANY_GROUP);
        aws->insert_option        ("all listed", "a", GMT_ALL_GROUPS);
        aws->insert_option        ("database",   "d", GMT_ALL_SPECIES);
        aws->update_option_menu();

        // trigger cleanup on popdown:
        aws->on_hide(makeWindowCallback(popdown_search_window_cb, &data));
        awgs = aws;
    }

    awgs->activate();
}

