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

#include <db_scanner.hxx>

#include <AW_rename.hxx>
#include <aw_select.hxx>
#include <aw_msg.hxx>
#include <aw_root.hxx>

#include <ad_cb.h>

#include <arb_progress.h>
#include <arb_str.h>
#include <arb_strbuf.h>
#include <arb_strarray.h>

#include <StrUniquifier.h>

const size_t INFO_WIDTH = 1000;

STATIC_ASSERT(INFO_WIDTH<=AW_selection_list_entry::MAX_DISPLAY_LENGTH);

void DbScanner::editfield_value_changed_cb() {
    if (ignore_editfield_change) return;

    LocallyModify<bool> do_not_recurse(ignore_editfield_change, true);

    bool update_self = false;
    {
        GB_begin_transaction(gb_main);

        GB_ERROR    error      = NULp;
        const char *field_name = awar_selected_field->read_char_pntr();

        if (!gb_item) {
            error = "No item selected";
        }
        else if (!field_name[0]) {
            error = "No field selected";
        }
        else if (!awar_edit_enabled->read_int()) { // edit disabled
            awar_editfield->write_string("");
            error = "Change ignored (edit is disabled!)";
        }
        else {
            char *value    = awar_editfield->read_string();
            int   vlen     = strlen(value);

            while (vlen>0 && value[vlen-1] == '\n') vlen--; // remove trailing newlines
            value[vlen]     = 0;

            GBDATA *gbd = search_selected_field();

            if (!gbd) { // field does not exist -> create new element
                if (vlen) {
                    GBDATA *gb_key      = GBT_get_changekey(gb_main, field_name, selector.change_key_path);
                    GBDATA *gb_key_type = GB_entry(gb_key, CHANGEKEY_TYPE);
                    GBDATA *gb_new      = GB_search(gb_item, field_name, (GB_TYPES)GB_read_int(gb_key_type));
                    if (!gb_new) error  = GB_await_error();
                    else    error       = GB_write_autoconv_string(gb_new, value);
                }
            }
            else { // change old element
                if (strcmp(field_name, "name") == 0) { // This is a real rename !!! // @@@ should use MutableItemSelector::id_field (no real problem)
                    if (speciesOrOrganism(selector.type)) { // species/organism
                        arb_progress  progress("Manual change of species ID");
                        const char   *name = GBT_get_name(gb_item);

                        if (!name) {
                            error = "cannot rename unnamed item";
                        }
                        else {
                            char *namedup = ARB_strdup(name);

                            if (strlen(value)) {
                                GBT_begin_rename_session(gb_main, 0);

                                error = GBT_rename_species(namedup, value, false);

                                if (error) GBT_abort_rename_session();
                                else error = GBT_commit_rename_session();
                            }
                            else {
                                error = AWTC_recreate_name(gb_item);
                            }

                            free(namedup);
                        }
                    }
                    else { // non-species (gene, experiment, etc.)
                        if (strlen(value)) {
                            GBDATA *gb_exists    = NULp;
                            GBDATA *gb_item_data = GB_get_father(gb_item);

                            for (gb_exists = selector.get_first_item(gb_item_data, QUERY_ALL_ITEMS);
                                 gb_exists;
                                 gb_exists = selector.get_next_item(gb_exists, QUERY_ALL_ITEMS))
                            {
                                if (ARB_stricmp(GBT_get_name_or_description(gb_exists), value) == 0) break;
                            }

                            if (gb_exists) error = GBS_global_string("There is already a %s named '%s'", selector.item_name, value);
                            else error           = GB_write_autoconv_string(gbd, value);
                        }
                        else {
                            error = "The 'name' field can't be empty.";
                        }
                    }

                    if (!error) update_self = true;
                }
                else {
                    if (vlen) {
                        error = GB_write_autoconv_string(gbd, value);
                    }
                    else {
                        if (GB_child(gbd)) {
                            error = "Sorry, cannot perform a deletion.\n(The selected entry has child entries. Delete them first.)";
                        }
                        else {
                            error = GB_delete(gbd);
                        }
                    }
                }
            }
            free(value);
        }

        aw_message_if(GB_end_transaction(gb_main, error));
    }

    awar_selected_field->touch(); // force refresh of edit-box

    if (update_self) { // if the name changed -> rewrite awars AFTER transaction was closed
        GB_transaction ta(gb_main);

        char *my_id = selector.generate_item_id(gb_main, gb_item);
        selector.update_item_awars(gb_main, get_root(), my_id); // update awars (e.g. AWAR_SPECIES_NAME)
        free(my_id);
    }
}

void DbScanner::toggle_marked_cb() {
    if (gb_item && !ignore_marktoggle_change) {
        long flag = awar_mark->read_int();
        GB_transaction ta(gb_main);
        GB_write_flag(gb_item, flag);
    }
}

void DbScanner::remap_edit_box() {
    GB_transaction ta(gb_main);

    GBDATA *gb_wanted_field = NULp;
    if (awar_edit_enabled->read_int()) {
        gb_wanted_field = search_selected_field(); // map only if editing is allowed
    }

    if (gb_wanted_field != gb_field) {
        DatabaseCallback editbox_update_cb = makeDatabaseCallback(DbScanner::field_changed_cb, this);
        if (gb_field) GB_remove_callback(gb_field, GB_CB_CHANGED_OR_DELETED, editbox_update_cb);

        gb_field = gb_wanted_field; // change field

        if (gb_field) {
            GB_add_callback(gb_field, GB_CB_CHANGED_OR_DELETED, editbox_update_cb);
        }
        field_changed_cb(GB_CB_CHANGED);
    }
}

inline const char *awarname(const char *scanner_id, const char *entry) {
    return GBS_global_string("tmp/dbscan/%s/%s", scanner_id, entry);
}

void DbScanner::create_awars(const char *scanner_id, bool have_edit_field, bool have_edit_toggle, bool have_mark_toggle) {
    arb_assert(implicated(have_edit_toggle, have_edit_field));

    // create scanner-local AWARS
    const char *list_awarname = awarname(scanner_id, "list");
    if (get_root()->awar_no_error(list_awarname)) { // awar already exists
        GBK_terminatef("multiple scanners bound to AWAR '%s'", list_awarname);
    }
    awar_selected_field = get_root()->awar_string(list_awarname, "", AW_ROOT_DEFAULT);

    awar_mapped_item_ID = get_root()->awar_string(awarname(scanner_id, "id"), "<undef>", AW_ROOT_DEFAULT);
    if (have_edit_field) {
        awar_editfield = get_root()->awar_string(awarname(scanner_id, "edit"), "", AW_ROOT_DEFAULT);
        if (have_edit_toggle) {
            awar_edit_enabled = get_root()->awar_int(awarname(scanner_id, "edit_enable"), true, AW_ROOT_DEFAULT);
        }
    }
    if (have_mark_toggle) {
        awar_mark = get_root()->awar_int(awarname(scanner_id, "mark"), true, AW_ROOT_DEFAULT);
    }
}

void DbScanner::create_field_edit_widgets(const char *edit_pos_fig, const char *edit_enable_pos_fig) {
    arb_assert(edit_pos_fig);

    RootCallback   remap_rcb = makeRootCallback(DbScanner::remap_edit_box, this);
    WindowCallback remap_wcb = makeWindowCallback(DbScanner::remap_edit_box, this);

    if (edit_enable_pos_fig) {
        aws->at(edit_enable_pos_fig);
        aws->create_toggle(awar_edit_enabled->awar_name);
    }

    awar_selected_field->add_callback(remap_rcb);
    awar_mapped_item_ID->add_callback(remap_rcb); // refresh edit-field if item changes
    if (edit_enable_pos_fig) awar_edit_enabled->add_callback(remap_rcb);
    awar_editfield->add_callback(makeRootCallback(DbScanner::editfield_value_changed_cb, this));

    aws->at(edit_pos_fig);
    aws->create_text_field(awar_editfield->awar_name, 20, 10);
}


DbScanner *DbScanner::create(GBDATA         *gb_main,
                             const char     *scanner_id,
                             AW_window      *aws,
                             const char     *box_pos_fig,
                             const char     *edit_pos_fig,
                             const char     *edit_enable_pos_fig,
                             DB_SCANNERMODE  scannermode,
                             const char     *mark_pos_fig,
                             ItemSelector&   selector)
{
    /* create an unmapped scanner box and optionally some buttons,
     * the return value is used as handle to further scanner functions
     */

    DbScanner *scanner = new DbScanner(scannermode, selector, aws, gb_main);

    if (gb_main) GB_push_transaction(gb_main);

    scanner->create_awars(scanner_id, edit_pos_fig, edit_enable_pos_fig, mark_pos_fig);

    // create GUI elements
    aws->at(box_pos_fig);
    scanner->create_field_selection_list();
    if (mark_pos_fig) {
        aws->at(mark_pos_fig);
        scanner->create_mark_toggle();
    }
    if (edit_pos_fig) {
        scanner->create_field_edit_widgets(edit_pos_fig, edit_enable_pos_fig);
    }

    if (gb_main) GB_pop_transaction(gb_main);
    aws->set_popup_callback(makeWindowCallback(DbScanner::remap_item, scanner));
    return scanner;
}

void DbScanner::append_field_data(GBS_strstruct& buf, GB_TYPES type, GBDATA *gbd) {
    /*! insert 'type', protection and data into 'buf'.
     * note that 'gbd' may be NULp -> only insert type
     */

    buf.put(GB_type_2_char(type));

    if (gbd) { // existing entry
        buf.put(GB_read_security_write(gbd)+'0');
        buf.put(':');
        buf.put(' ');

        char *data = GB_read_as_string(gbd);
        if (data) {
            int rest    = INFO_WIDTH-buf.get_position();
            int ssize   = strlen(data);

            arb_assert(rest>0);
            arb_assert(ssize>=0);

            if (ssize > rest) {
                ssize = GBS_shorten_repeated_data(data);
                if (ssize > rest) {
                    if (ssize>5) strcpy(data+rest-5, "[...]");
                    ssize = rest;
                }
            }

            buf.ncat(data, ssize);
            free(data);
        }
        else {
            buf.ncat("<unprintable>", 13);
        }
    }
    else { // non-existing entry
        buf.ncat(" :", 2);
    }
}



int DbScanner::fill_fields_recursive(GBDATA *gbd, const int depth, GBS_strstruct& buf) {
    static StrUniquifier trackKeys; // track keys with identical names and assign unique IDs.

    int max_keyname_length = 0;
    arb_assert(buf.empty());
    for (int i = 0; i<depth; ++i) buf.ncat(": ", 2);

    const char *key    = GB_read_key_pntr(gbd);
    int         keylen = strlen(key);
    buf.ncatPadded(key, keylen, previous_max_keyname_length);
    buf.put(' ');
    max_keyname_length = std::max(max_keyname_length, keylen);

    bool isContainer = GB_read_type(gbd) == GB_DB;
    if (isContainer) buf.ncat("<CONTAINER>:", 12);
    else             append_field_data(buf, GB_read_type(gbd), gbd);

    {
        const char *id = trackKeys.make_unique_key(key); // create unique IDs for multiple fields with same key
        field_sel->insert(buf.get_data(), id);
        buf.erase();
    }

    if (isContainer) {
        for (GBDATA *gb2 = GB_child(gbd); gb2; gb2 = GB_nextChild(gb2)) {
            max_keyname_length = std::max(max_keyname_length, fill_fields_recursive(gb2, depth+1, buf));
        }
    }

    // cleanup static variable (at top of recursion):
    if (!depth) trackKeys.clear();

    return max_keyname_length;
}

int DbScanner::fill_fields_by_keydata(GBS_strstruct& buf) {
    int max_keyname_length = 0;

    GBDATA *gb_key_data = GB_search(gb_main, get_selector().change_key_path, GB_CREATE_CONTAINER);

    for (int existing = 1; existing >= 0; --existing) {
        for (GBDATA *gb_key = GB_entry(gb_key_data, CHANGEKEY); gb_key; gb_key = GB_nextEntry(gb_key)) {
            GBDATA *gb_key_hidden = GB_entry(gb_key, CHANGEKEY_HIDDEN);
            if (gb_key_hidden && GB_read_int(gb_key_hidden)) continue; // don't show hidden fields in 'species information' window

            GBDATA *gb_key_name = GB_entry(gb_key, CHANGEKEY_NAME);
            if (!gb_key_name) continue;

            GBDATA *gb_key_type = GB_entry(gb_key, CHANGEKEY_TYPE);

            const char *key = GB_read_char_pntr(gb_key_name);
            GBDATA     *gbd = GB_search(gb_item, key, GB_FIND);

            if ((!existing) == (!gbd)) { // first print only existing; then non-existing entries
                buf.erase();

                int keylen         = strlen(key);
                buf.ncatPadded(key, keylen, previous_max_keyname_length);
                buf.put(' ');
                max_keyname_length = std::max(max_keyname_length, keylen);

                GB_TYPES type = GB_TYPES(GB_read_int(gb_key_type));
                append_field_data(buf, type, gbd);

                field_sel->insert(buf.get_data(), key);
            }
        }
    }

    return max_keyname_length;
}

void DbScanner::field_changed_cb(GB_CB_TYPE cbtype) {
    if (cbtype == GB_CB_DELETE) {
        gb_field = NULp;
    }

    LocallyModify<bool> just_a_refresh(ignore_editfield_change, true);

    if (gb_field) {
        char *data = GB_read_as_string(gb_field);
        if (!data) data = strdup("<you cannot edit this datatype>");
        awar_editfield->write_string(data);
        free(data);
    }
    else {
        awar_editfield->write_string("");
    }
}

inline bool width_adjusted(int& prevMaxLen, int currMaxLen) {
    bool adjusted = false;
    if (prevMaxLen != currMaxLen) {
        prevMaxLen = currMaxLen;
        adjusted = true;
    }
    return adjusted;
}

void DbScanner::changed_cb(GB_CB_TYPE cbtype) {
    // performs updates necessary when mapped item changes:
    // - refresh field selection list
    // - update display of item name

    if (cbtype == GB_CB_DELETE) {
        gb_item = NULp;
    }
    if (gb_item && !aws->is_shown()) {
        return;
    }

    // update selection list
    field_sel->clear();
    if (gb_item) {
        GB_transaction ta(gb_main);
        GBS_strstruct buf;

        int fillCount = 0;
        while (true) {
            int max_keyname_length = 0;
            switch (scannermode) {
                case DB_SCANNER:   max_keyname_length = fill_fields_recursive(gb_item, 0, buf); break;
                case DB_KEYVIEWER: max_keyname_length = fill_fields_by_keydata(buf); break;
            }
            fillCount++;

            if (!width_adjusted(previous_max_keyname_length, max_keyname_length)) break;

            // otherwise occurring keyname length changed. refill (once) to fix used width.
            if (fillCount>=2) {
                arb_assert(0); // formatting should not fail multiple times
                break;
            }
            field_sel->clear();
        }
    }

    field_sel->insert_default("", "");
    field_sel->update();

    if (gb_item) {
        GB_transaction ta(gb_main);

        if (awar_mark) {
            LocallyModify<bool> ignoreUpdate(ignore_marktoggle_change, true);
            long flag = GB_read_flag(gb_item);
            awar_mark->write_int(flag);
        }

        char *id = get_mapped_item_id();
        awar_mapped_item_ID->write_string(id);
        free(id);
    }
    else {
        awar_mapped_item_ID->write_string("<none selected>");
    }
    awar_selected_field->touch();
}

void DbScanner::keydata_modified_cb(GB_CB_TYPE cbtype) {
    // unmap edit field if 'key_data' has been changed (entries might have been deleted)

    bool deselect = false;
    {
        const char *field_name = awar_selected_field->read_char_pntr();
        if (field_name[0]) {
            GBDATA *gb_key = GBT_get_changekey(gb_main, field_name, selector.change_key_path);

            if (!gb_key) { // key unknown
                deselect = true;
            }
            else {
                GBDATA *gb_key_hidden = GB_entry(gb_key, CHANGEKEY_HIDDEN);
                if (gb_key_hidden && GB_read_int(gb_key_hidden)) { // key hidden -> deselect
                    deselect = true;
                }
            }
        }
    }

    if (deselect) awar_selected_field->write_string(""); // deselect field
    changed_cb(cbtype);
}

void DbScanner::Map(GBDATA *gb_new_item, const char *key_path) {
    GB_transaction ta(gb_main);

    arb_assert(implicated(scannermode == DB_KEYVIEWER, key_path != NULp));

    GBDATA *gb_key_data = scannermode == DB_KEYVIEWER ? GB_search(gb_main, key_path, GB_CREATE_CONTAINER) : NULp;

    if (gb_item) {
        GB_remove_callback(gb_item, GB_CB_CHANGED_OR_DELETED, makeDatabaseCallback(DbScanner::changed_cb, this));
        if (scannermode == DB_KEYVIEWER) {
            GB_remove_callback(gb_key_data, GB_CB_CHANGED, makeDatabaseCallback(DbScanner::keydata_modified_cb, this));
        }
    }

    gb_item = gb_new_item;

    if (gb_item) {
        GB_add_callback(gb_item, GB_CB_CHANGED_OR_DELETED, makeDatabaseCallback(DbScanner::changed_cb, this));
        if (scannermode == DB_KEYVIEWER) {
            GB_add_callback(gb_key_data, GB_CB_CHANGED, makeDatabaseCallback(DbScanner::keydata_modified_cb, this));
        }
    }

    changed_cb(GB_CB_CHANGED);
}

void DbScanner::remap_item() {
    if (gb_main) {
        GB_transaction ta(gb_main);
        Map(selector.get_selected_item(gb_main, AW_root::SINGLETON), selector.change_key_path);
    }
}

void collectKeysRegisteredInDatabase(StrArray& fields, GBDATA *gb_main, ItemSelector& sel, bool skipContainers, bool skipHidden) {
    arb_assert(gb_main);

    GB_transaction ta(gb_main);

    GBDATA *gb_key_data = GB_search(gb_main, sel.change_key_path, GB_FIND);
    GBDATA *gb_key      = gb_key_data ? GB_entry(gb_key_data, CHANGEKEY) : NULp;

    while (gb_key) {
        bool skipThis = false;
        if (!skipThis && skipContainers) {
            GB_TYPES type = GB_TYPES(*GBT_read_int(gb_key, CHANGEKEY_TYPE));
            skipThis      = type == GB_DB; // skip when type is container
        }
        if (!skipThis && skipHidden) {
            GBDATA *gb_key_hidden = GB_entry(gb_key, CHANGEKEY_HIDDEN);
            skipThis = gb_key_hidden && GB_read_int(gb_key_hidden); // skip when hidden
        }

        if (!skipThis) {
            char *key_name = GBT_read_string(gb_key, CHANGEKEY_NAME);
            fields.put(key_name);
        }

        GBDATA *gb_next_key = GB_nextEntry(gb_key);
        gb_key              = gb_next_key;
    }
}

