/**
 * UGENE - Integrated Bioinformatics Tools.
 * Copyright (C) 2008-2025 UniPro <ugene@unipro.ru>
 * http://ugene.net
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301, USA.
 */

#include "AnnotationsTreeView.h"

#include <QApplication>
#include <QClipboard>
#include <QDrag>
#include <QHeaderView>
#include <QLineEdit>
#include <QMap>
#include <QMenu>
#include <QMessageBox>
#include <QPainter>
#include <QStack>
#include <QToolTip>
#include <QVBoxLayout>

#include <U2Core/AnnotationModification.h>
#include <U2Core/AnnotationSelection.h>
#include <U2Core/AnnotationSettings.h>
#include <U2Core/AnnotationTableObject.h>
#include <U2Core/AppContext.h>
#include <U2Core/AppSettings.h>
#include <U2Core/AutoAnnotationsSupport.h>
#include <U2Core/ClipboardController.h>
#include <U2Core/CollectionUtils.h>
#include <U2Core/DBXRefRegistry.h>
#include <U2Core/DNASequenceObject.h>
#include <U2Core/DNASequenceSelection.h>
#include <U2Core/GenbankFeatures.h>
#include <U2Core/L10n.h>
#include <U2Core/QObjectScopedPointer.h>
#include <U2Core/Settings.h>
#include <U2Core/TaskSignalMapper.h>
#include <U2Core/U1AnnotationUtils.h>
#include <U2Core/U2SafePoints.h>
#include <U2Core/UserApplicationsSettings.h>

#include <U2Gui/CreateAnnotationDialog.h>
#include <U2Gui/CreateAnnotationWidgetController.h>
#include <U2Gui/EditQualifierDialog.h>
#include <U2Gui/GUIUtils.h>
#include <U2Gui/OpenViewTask.h>
#include <U2Gui/ProjectTreeController.h>
#include <U2Gui/ProjectTreeItemSelectorDialog.h>
#include <U2Gui/TreeWidgetUtils.h>

#include "ADVConstants.h"
#include "ADVSequenceObjectContext.h"
#include "AnnotatedDNAView.h"
#include "AutoAnnotationUtils.h"

namespace U2 {

/************************************************************************/
/* TreeSorter */
/************************************************************************/
class TreeSorter {
public:
    TreeSorter(AnnotationsTreeView* t)
        : w(t) {
        w->setSortingEnabled(false);
    }
    virtual ~TreeSorter() {
        w->setSortingEnabled(true);
    }

private:
    AnnotationsTreeView* w;
};

/************************************************************************/
/* AnnotationsTreeWidget */
/************************************************************************/
AnnotationsTreeWidget::AnnotationsTreeWidget(QWidget* parent)
    : QTreeWidget(parent) {
}

QTreeWidgetItem* AnnotationsTreeWidget::itemFromIndex(const QModelIndex& index) const {
    return QTreeWidget::itemFromIndex(index);
}

/************************************************************************/
/* AnnotationsTreeView */
/************************************************************************/
#define SETTINGS_ROOT QString("view_adv/annotations_tree_view/")
#define COLUMN_SIZES QString("columnSizes")

const int AnnotationsTreeView::COLUMN_NAME = 0;
const int AnnotationsTreeView::COLUMN_TYPE = 1;
const int AnnotationsTreeView::COLUMN_VALUE = 2;

const QString AnnotationsTreeView::annotationMimeType = "application/x-annotations-and-groups";
AVGroupItem* AnnotationsTreeView::dropDestination = nullptr;
QList<Annotation*> AnnotationsTreeView::dndAdded = QList<Annotation*>();

AnnotationsTreeView::AnnotationsTreeView(AnnotatedDNAView* _ctx)
    : ctx(_ctx), dndHit(0) {
    lastMB = Qt::NoButton;
    lastClickedColumn = 0;

    tree = new AnnotationsTreeWidget(this);
    tree->setObjectName("annotations_tree_widget");

    tree->setSortingEnabled(true);
    tree->sortItems(0, Qt::AscendingOrder);

    tree->setColumnCount(3);
    headerLabels << tr("Name") << tr("Type") << tr("Value");

    tree->setHeaderLabels(headerLabels);
    tree->setUniformRowHeights(true);
    tree->setSelectionMode(QAbstractItemView::ExtendedSelection);
    tree->viewport()->installEventFilter(this);
    tree->setMouseTracking(true);
    tree->setAutoScroll(true);

    connect(tree, SIGNAL(itemEntered(QTreeWidgetItem*, int)), SLOT(sl_itemEntered(QTreeWidgetItem*, int)));
    connect(tree, SIGNAL(itemClicked(QTreeWidgetItem*, int)), SLOT(sl_itemClicked(QTreeWidgetItem*, int)));
    connect(tree, SIGNAL(itemPressed(QTreeWidgetItem*, int)), SLOT(sl_itemPressed(QTreeWidgetItem*)));
    connect(tree, SIGNAL(itemDoubleClicked(QTreeWidgetItem*, int)), SLOT(sl_itemDoubleClicked(QTreeWidgetItem*, int)));
    connect(tree, SIGNAL(itemExpanded(QTreeWidgetItem*)), SLOT(sl_itemExpanded(QTreeWidgetItem*)));

    auto layout = new QVBoxLayout();
    layout->setContentsMargins(0, 0, 0, 0);
    layout->addWidget(tree);
    setLayout(layout);

    restoreWidgetState();

    connect(ctx, &GObjectViewController::si_buildMenu, this, &AnnotationsTreeView::sl_onBuildMenu);
    connect(ctx, SIGNAL(si_annotationObjectAdded(AnnotationTableObject*)), SLOT(sl_onAnnotationObjectAdded(AnnotationTableObject*)));
    connect(ctx, SIGNAL(si_annotationObjectRemoved(AnnotationTableObject*)), SLOT(sl_onAnnotationObjectRemoved(AnnotationTableObject*)));
    connect(ctx, SIGNAL(si_sequenceAdded(ADVSequenceObjectContext*)), SLOT(sl_sequenceAdded(ADVSequenceObjectContext*)));
    connect(ctx, SIGNAL(si_sequenceRemoved(ADVSequenceObjectContext*)), SLOT(sl_sequenceRemoved(ADVSequenceObjectContext*)));
    QList<AnnotationTableObject*> aObjs = ctx->getAnnotationObjects(true);
    foreach (AnnotationTableObject* obj, aObjs) {
        sl_onAnnotationObjectAdded(obj);
    }
    const QList<ADVSequenceObjectContext*> seqContexts = ctx->getSequenceContexts();
    foreach (ADVSequenceObjectContext* context, seqContexts) {
        connectSequenceObjectContext(context);
    }
    connectAnnotationSelection();
    connectAnnotationGroupSelection();
    connect(tree, SIGNAL(itemSelectionChanged()), SLOT(sl_onItemSelectionChanged()));

    connect(AppContext::getAnnotationsSettingsRegistry(), SIGNAL(si_annotationSettingsChanged(const QStringList&)), SLOT(sl_onAnnotationSettingsChanged(const QStringList&)));

#define SORT_INTERVAL 500
    sortTimer.setInterval(SORT_INTERVAL);
    sortTimer.setSingleShot(true);
    connect(&sortTimer, SIGNAL(timeout()), SLOT(sl_sortTree()));

    addColumnIcon = QIcon(":core/images/add_column.png");
    removeColumnIcon = QIcon(":core/images/remove_column.png");

    /*
    pasteAction = new QAction(QIcon(":/core/images/paste.png"), tr("Paste annotations"), this);
    pasteAction->setObjectName("Paste annotations");
    pasteAction->setShortcut(QKeySequence(Qt::SHIFT| Qt::Key_V | Qt::ControlModifier));
    pasteAction->setShortcutContext(Qt::WidgetShortcut);
    connect(pasteAction, SIGNAL(triggered()), SLOT(sl_paste()));
    tree->addAction(pasteAction);
    */

    addAnnotationObjectAction = new QAction(tr("Objects with annotations..."), this);
    connect(addAnnotationObjectAction, SIGNAL(triggered()), SLOT(sl_onAddAnnotationObjectToView()));

    removeObjectsFromViewAction = new QAction(tr("Selected objects with annotations from view"), this);
    removeObjectsFromViewAction->setObjectName("Selected objects with annotations from view");
    removeObjectsFromViewAction->setShortcut(QKeySequence(Qt::SHIFT | Qt::Key_Delete));
    removeObjectsFromViewAction->setShortcutContext(Qt::WidgetShortcut);
    connect(removeObjectsFromViewAction, SIGNAL(triggered()), SLOT(sl_removeObjectFromView()));
    tree->addAction(removeObjectsFromViewAction);

    removeAnnsAndQsAction = new QAction(tr("Selected annotations and qualifiers"), this);
    removeAnnsAndQsAction->setObjectName("Selected annotations and qualifiers");
    // hotkeys are set in AnnotatedDNAView::createWidget()
    connect(removeAnnsAndQsAction, SIGNAL(triggered()), SLOT(sl_removeAnnsAndQs()));
    tree->addAction(removeAnnsAndQsAction);

    copyQualifierURLAction = new QAction(tr("Copy qualifier URL"), this);
    connect(copyQualifierURLAction, SIGNAL(triggered()), SLOT(sl_onCopyQualifierURL()));

    toggleQualifierColumnAction = new QAction(tr("Toggle column"), this);
    toggleQualifierColumnAction->setObjectName("toggle_column");
    connect(toggleQualifierColumnAction, SIGNAL(triggered()), SLOT(sl_onToggleQualifierColumn()));

    removeColumnByHeaderClickAction = new QAction(tr("Hide column"), this);
    removeColumnByHeaderClickAction->setIcon(removeColumnIcon);
    connect(removeColumnByHeaderClickAction, SIGNAL(triggered()), SLOT(sl_onRemoveColumnByHeaderClick()));

    searchQualifierAction = new QAction(tr("Find qualifier..."), this);
    searchQualifierAction->setObjectName("find_qualifier_action");
    searchQualifierAction->setIcon(QIcon(":core/images/zoom_whole.png"));
    connect(searchQualifierAction, SIGNAL(triggered()), SLOT(sl_searchQualifier()));

    invertAnnotationSelectionAction = new QAction(tr("Invert annotation selection"), this);
    invertAnnotationSelectionAction->setObjectName("invert_selection_action");
    connect(invertAnnotationSelectionAction, SIGNAL(triggered()), SLOT(sl_invertSelection()));

    copyColumnTextAction = new QAction(tr("Copy column text"), this);
    connect(copyColumnTextAction, SIGNAL(triggered()), SLOT(sl_onCopyColumnText()));

    copyColumnURLAction = new QAction(tr("copy column URL"), this);
    connect(copyColumnURLAction, SIGNAL(triggered()), SLOT(sl_onCopyColumnURL()));

    editAction = new QAction(tr("Annotation"), this);
    editAction->setObjectName("edit_annotation_tree_item");
    editAction->setShortcut(QKeySequence(Qt::Key_F2));
    editAction->setShortcutContext(Qt::WindowShortcut);
    connect(editAction, SIGNAL(triggered()), SLOT(sl_edit()));
    tree->addAction(editAction);

    addQualifierAction = new QAction(tr("Qualifier..."), this);
#ifndef Q_OS_DARWIN
    addQualifierAction->setShortcut(QKeySequence(Qt::Key_Insert));
#else
    addQualifierAction->setShortcut(QKeySequence(Qt::SHIFT + Qt::Key_I));
#endif
    addQualifierAction->setShortcutContext(Qt::WindowShortcut);
    addQualifierAction->setObjectName("add_qualifier_action");
    connect(addQualifierAction, SIGNAL(triggered()), SLOT(sl_addQualifier()));
    tree->addAction(addQualifierAction);

    exportAutoAnnotationsGroup = new QAction(tr("Make auto-annotations persistent"), this);
    connect(exportAutoAnnotationsGroup, SIGNAL(triggered()), SLOT(sl_exportAutoAnnotationsGroup()));

    updateState();

    isDragging = false;
    resetDragAndDropData();
    tree->setAcceptDrops(true);
}

void AnnotationsTreeView::restoreWidgetState() {
    QMap<QString, QVariant> geom = AppContext::getSettings()->getValue(SETTINGS_ROOT + COLUMN_SIZES).toMap();
    bool ok = false;
    if (!geom.isEmpty()) {
        foreach (const QString& columnId, geom.keys()) {
            int columnIndex = columnId.toInt(&ok);
            if (!ok || columnIndex < 0 || columnIndex >= tree->columnCount()) {
                ok = false;
                break;
            }
            int width = geom[columnId].toInt(&ok);
            if (!ok || width <= 0) {
                ok = false;
                break;
            }
            tree->setColumnWidth(columnIndex, width);
        }
    }
    if (!ok) {
        tree->setColumnWidth(COLUMN_NAME, 300);
        tree->setColumnWidth(COLUMN_TYPE, 150);
    }
}

void AnnotationsTreeView::saveWidgetState() {
    QMap<QString, QVariant> geom;
    const int n = tree->columnCount();
    for (int i = 0; i < n; i++) {
        geom.insert(QString::number(i), tree->columnWidth(i));
    }
    AppContext::getSettings()->setValue(SETTINGS_ROOT + COLUMN_SIZES, geom);
}

AVGroupItem* AnnotationsTreeView::findGroupItem(AnnotationGroup* g) const {
    if (g->getParentGroup() == nullptr) {
        const int n = tree->topLevelItemCount();
        for (int i = 0; i < n; i++) {
            auto item = static_cast<AVItem*>(tree->topLevelItem(i));
            SAFE_POINT(AVItemType_Group == item->type, "Invalid item type!", nullptr);
            auto groupItem = static_cast<AVGroupItem*>(item);
            if (groupItem->group == g) {
                return groupItem;
            }
        }
    } else {
        AVGroupItem* parentGroupItem = findGroupItem(g->getParentGroup());
        if (parentGroupItem != nullptr) {
            const int n = parentGroupItem->childCount();
            for (int i = 0; i < n; i++) {
                auto item = static_cast<AVItem*>(parentGroupItem->child(i));
                if (AVItemType_Group != item->type) {
                    continue;
                }
                auto gItem = static_cast<AVGroupItem*>(item);
                if (gItem->group == g) {
                    return gItem;
                }
            }
        }
    }
    return nullptr;
}

AVAnnotationItem* AnnotationsTreeView::findAnnotationItem(const AVGroupItem* groupItem, Annotation* a) const {
    for (int i = 0, n = groupItem->childCount(); i < n; i++) {
        auto item = static_cast<AVItem*>(groupItem->child(i));
        if (item->type != AVItemType_Annotation) {
            continue;
        }
        auto aItem = static_cast<AVAnnotationItem*>(item);
        if (aItem->annotation == a) {
            return aItem;
        }
    }
    return nullptr;
}

AVAnnotationItem* AnnotationsTreeView::findAnnotationItem(AnnotationGroup* g, Annotation* a) const {
    AVGroupItem* groupItem = findGroupItem(g);
    if (groupItem == nullptr) {
        return nullptr;
    }
    return findAnnotationItem(groupItem, a);
}

/** This method is optimized to use annotation groups.
So can only be used for annotations that belongs to some object */
QList<AVAnnotationItem*> AnnotationsTreeView::findAnnotationItems(Annotation* a) const {
    QList<AVAnnotationItem*> res;

    SAFE_POINT(a->getGObject() != nullptr && ctx->getAnnotationObjects(true).contains(a->getGObject()), "Invalid annotation table!", res);

    AnnotationGroup* g = a->getGroup();
    AVGroupItem* gItem = findGroupItem(g);
    SAFE_POINT(gItem != nullptr, "AnnotationItemGroup not found!", res);
    AVAnnotationItem* aItem = findAnnotationItem(gItem, a);
    SAFE_POINT(aItem != nullptr, "AnnotationItem not found!", res);
    res.append(aItem);

    return res;
}

QList<AVAnnotationItem*> AnnotationsTreeView::findAnnotationItems(const AVGroupItem* gi) const {
    QList<AVAnnotationItem*> annotationItems;
    for (int i = 0; i < gi->childCount(); ++i) {
        auto childItem = dynamic_cast<AVItem*>(gi->child(i));
        SAFE_POINT(childItem != nullptr, "Can't cast 'QTreeWidgetItem *' to 'AVItem *'", QList<AVAnnotationItem*>());
        switch (childItem->type) {
            case AVItemType_Annotation: {
                auto annotationItem = dynamic_cast<AVAnnotationItem*>(childItem);
                SAFE_POINT(annotationItem != nullptr, "Can't cast 'AVItem *' to 'AVAnnotationItem *'", QList<AVAnnotationItem*>());
                annotationItems << annotationItem;
                break;
            }
            case AVItemType_Group: {
                auto groupItem = dynamic_cast<AVGroupItem*>(childItem);
                SAFE_POINT(groupItem != nullptr, "Can't cast 'AVItem *' to 'AVGroupItem *'", QList<AVAnnotationItem*>());
                annotationItems << findAnnotationItems(groupItem);
                break;
            }
            default:;  // Do nothing
        }
    }
    return annotationItems;
}

void AnnotationsTreeView::removeGroupAnnotationsFromCache(const AVGroupItem* groupItem) {
    CHECK(!selectedAnnotation.isEmpty(), );
    const QSet<AVAnnotationItem*> annotationItems = toSet(findAnnotationItems(groupItem));
    foreach (AVAnnotationItem* annotationItem, selectedAnnotation.keys()) {
        if (annotationItems.contains(annotationItem)) {
            selectedAnnotation.remove(annotationItem);
        }
    }
}

void AnnotationsTreeView::connectSequenceObjectContext(ADVSequenceObjectContext* advContext) {
    connect(advContext, SIGNAL(si_annotationActivated(Annotation*, int)), SLOT(sl_annotationActivated(Annotation*, int)));
    connect(advContext, SIGNAL(si_annotationDoubleClicked(Annotation*, int)), SLOT(sl_annotationDoubleClicked(Annotation*, int)));
    connect(advContext, SIGNAL(si_clearSelectedAnnotationRegions()), SLOT(sl_clearSelectedAnnotations()));
}

void AnnotationsTreeView::disconnectSequenceObjectContext(ADVSequenceObjectContext* advContext) {
    advContext->disconnect(this);
}

void AnnotationsTreeView::connectAnnotationSelection() {
    connect(ctx->getAnnotationsSelection(),
            SIGNAL(si_selectionChanged(AnnotationSelection*, const QList<Annotation*>&, const QList<Annotation*>&)),
            SLOT(sl_onAnnotationSelectionChanged(AnnotationSelection*, const QList<Annotation*>&, const QList<Annotation*>&)));
}

void AnnotationsTreeView::connectAnnotationGroupSelection() {
    connect(ctx->getAnnotationsGroupSelection(),
            SIGNAL(si_selectionChanged(AnnotationGroupSelection*, const QList<AnnotationGroup*>&, const QList<AnnotationGroup*>&)),
            SLOT(sl_onAnnotationGroupSelectionChanged(AnnotationGroupSelection*, const QList<AnnotationGroup*>&, const QList<AnnotationGroup*>&)));
}

void AnnotationsTreeView::sl_onItemSelectionChanged() {
    AnnotationSelection* annotationsSelection = ctx->getAnnotationsSelection();
    QList<Annotation*> selectedAnnotationsBefore = annotationsSelection->getAnnotations();
    annotationsSelection->disconnect(this);
    annotationsSelection->clear();

    AnnotationGroupSelection* annotationsGroupSelection = ctx->getAnnotationsGroupSelection();
    annotationsGroupSelection->disconnect(this);
    annotationsGroupSelection->clear();

    bool annotationActivated = false;
    QList<QTreeWidgetItem*> selectedTreeItems = tree->selectedItems();
    foreach (QTreeWidgetItem* i, selectedTreeItems) {
        auto item = static_cast<AVItem*>(i);
        if (item->type == AVItemType_Annotation) {
            auto annotationItem = static_cast<AVAnnotationItem*>(item);
            Annotation* annotation = annotationItem->annotation;
            SAFE_POINT(annotation->getGObject() != nullptr, "Invalid annotation table!", );
            // Activate the first new annotation from the selected tree items.
            if (!annotationActivated && !selectedAnnotationsBefore.contains(annotation)) {
                emitAnnotationActivated(annotation);
                annotationActivated = true;
            }
            annotationsSelection->add(annotation);
        } else if (item->type == AVItemType_Group) {
            auto groupItem = static_cast<AVGroupItem*>(item);
            annotationsGroupSelection->addToSelection(groupItem->group);
        }
    }

    // make the text of Edit action correct
    if (selectedTreeItems.size() == 1) {
        switch (static_cast<AVItem*>(selectedTreeItems.last())->type) {
            case AVItemType_Group:
                editAction->setText(tr("Group"));
                break;
            case AVItemType_Qualifier:
                editAction->setText(tr("Qualifier"));
                break;
            case AVItemType_Annotation:
                editAction->setText(tr("Annotation"));
                break;
            default:
                FAIL("Invalid annotation tree item type", );
        }
    }

    connectAnnotationSelection();
    connectAnnotationGroupSelection();
    updateState();
}

void AnnotationsTreeView::sl_onAnnotationSelectionChanged(AnnotationSelection*, const QList<Annotation*>& added, const QList<Annotation*>& removed) {
    tree->disconnect(this, SIGNAL(sl_onItemSelectionChanged()));
    clearSelectedNotAnnotations();
    foreach (Annotation* a, removed) {
        AnnotationGroup* g = a->getGroup();
        AVAnnotationItem* item = findAnnotationItem(g, a);
        if (item == nullptr) {
            continue;
        }
        if (item->isSelected()) {
            item->setSelected(false);
        }
    }
    AVAnnotationItem* toVisible = nullptr;
    QList<AVAnnotationItem*> selectedItems;
    foreach (Annotation* a, added) {
        AnnotationGroup* g = a->getGroup();
        AVAnnotationItem* item = findAnnotationItem(g, a);
        if (item == nullptr) {
            continue;
        }
        if (!item->isSelected()) {
            item->setSelected(true);
            selectedItems.append(item);
            for (QTreeWidgetItem* p = item->parent(); p != nullptr; p = p->parent()) {
                if (!p->isExpanded()) {
                    p->setExpanded(true);
                }
            }
        }
        toVisible = item;
    }

    if (!selectedItems.isEmpty()) {
        tree->setCurrentItem(selectedItems.last(), 0, QItemSelectionModel::NoUpdate);
        editAction->setText(tr("Annotation"));
    }

    connect(tree, SIGNAL(itemSelectionChanged()), SLOT(sl_onItemSelectionChanged()));

    // make item visible if special conditions are met
    if (toVisible != nullptr && added.size() == 1) {
        tree->scrollToItem(toVisible, QAbstractItemView::EnsureVisible);
    }
    updateState();
}

void AnnotationsTreeView::sl_onAnnotationGroupSelectionChanged(AnnotationGroupSelection* s, const QList<AnnotationGroup*>& added, const QList<AnnotationGroup*>& removed) {
    Q_UNUSED(s);

    foreach (AnnotationGroup* g, removed) {
        AVGroupItem* item = findGroupItem(g);
        if (item->isSelected()) {
            item->setSelected(false);
        }
    }

    foreach (AnnotationGroup* g, added) {
        AVGroupItem* item = findGroupItem(g);
        if (!item->isSelected()) {
            item->setSelected(true);
        }
    }

    if (added.size() == 1) {
        AVGroupItem* item = findGroupItem(added.first());
        tree->scrollToItem(item, QAbstractItemView::EnsureVisible);
    }
}

void AnnotationsTreeView::sl_onAnnotationObjectAdded(AnnotationTableObject* obj) {
    TreeSorter ts(this);
    Q_UNUSED(ts);

    SAFE_POINT(findGroupItem(obj->getRootGroup()) == nullptr, "Invalid annotation group!", );

    AVGroupItem* groupItem = buildGroupTree(nullptr, obj->getRootGroup(), false);
    SAFE_POINT(groupItem != nullptr, "creating AVGroupItem failed", );
    tree->addTopLevelItem(groupItem);
    connect(obj, SIGNAL(si_onAnnotationsAdded(const QList<Annotation*>&)), SLOT(sl_onAnnotationsAdded(const QList<Annotation*>&)));
    connect(obj, SIGNAL(si_onAnnotationsRemoved(const QList<Annotation*>&)), SLOT(sl_onAnnotationsRemoved(const QList<Annotation*>&)));
    connect(obj, SIGNAL(si_onAnnotationsModified(const QList<AnnotationModification>&)), SLOT(sl_onAnnotationsModified(const QList<AnnotationModification>&)));

    connect(obj, SIGNAL(si_onGroupCreated(AnnotationGroup*)), SLOT(sl_onGroupCreated(AnnotationGroup*)));
    connect(obj, SIGNAL(si_onGroupRemoved(AnnotationGroup*, AnnotationGroup*)), SLOT(sl_onGroupRemoved(AnnotationGroup*, AnnotationGroup*)));
    connect(obj, SIGNAL(si_onGroupRenamed(AnnotationGroup*)), SLOT(sl_onGroupRenamed(AnnotationGroup*)));

    connect(obj, SIGNAL(si_modifiedStateChanged()), SLOT(sl_annotationObjectModifiedStateChanged()));
    connect(obj, SIGNAL(si_nameChanged(const QString&)), SLOT(sl_onAnnotationObjectRenamed(const QString&)));
}

void AnnotationsTreeView::sl_onAnnotationObjectRemoved(AnnotationTableObject* obj) {
    TreeSorter ts(this);
    Q_UNUSED(ts);

    AVGroupItem* groupItem = findGroupItem(obj->getRootGroup());
    removeGroupAnnotationsFromCache(groupItem);
    delete groupItem;

    obj->disconnect(this);
}

void AnnotationsTreeView::sl_onAnnotationObjectRenamed(const QString&) {
    auto ao = qobject_cast<AnnotationTableObject*>(sender());
    AVGroupItem* gi = findGroupItem(ao->getRootGroup());
    SAFE_POINT(gi != nullptr, "Failed to find annotations object on rename!", );
    gi->updateVisual();
}

namespace {

bool doesGroupPresentInList(const QList<AnnotationGroup*>& list, AnnotationGroup* group) {
    foreach (AnnotationGroup* groupFromList, list) {
        if (groupFromList == group || groupFromList->isParentOf(group)) {
            return true;
        }
    }
    return false;
}

}  // namespace

void AnnotationsTreeView::sl_onAnnotationsAdded(const QList<Annotation*>& as) {
    TreeSorter ts(this);

    QSet<AVGroupItem*> toUpdate;
    QList<AnnotationGroup*> createdGroups;
    foreach (Annotation* a, as) {
        AnnotationGroup* ag = a->getGroup();
        if (doesGroupPresentInList(createdGroups, ag)) {
            continue;
        }
        AVGroupItem* gi = findGroupItem(ag);
        if (gi != nullptr) {
            buildAnnotationTree(gi, a);
        } else {
            AnnotationGroup* childGroup = ag;
            while (true) {
                gi = findGroupItem(childGroup->getParentGroup());
                if (gi != nullptr) {
                    break;
                }
                childGroup = childGroup->getParentGroup();
            }
            SAFE_POINT(gi != nullptr, "AnnotationsTreeView::sl_onAnnotationsAdded: childGroup not found", );
            buildGroupTree(gi, childGroup);
            createdGroups << childGroup;  // if a group item has been built it already contains corresponding annotation items
                                          // so in further iterations we skip child annotations of this group
        }
        SAFE_POINT(gi != nullptr, "Invalid annotation view item!", );
        toUpdate.insert(gi);

        if (dndAdded.contains(a)) {
            dndHit++;
            if (dndHit == dndAdded.size()) {
                if (!ctx->areAnnotationsInRange(dndAdded)) {
                    QMessageBox::warning(this, L10N::warningTitle(), tr("At least one dragged annotation is out of the sequence range!"));
                }
                dndHit = 0;
            }
        }
    }
    while (!toUpdate.isEmpty()) {
        AVGroupItem* i = *toUpdate.begin();
        toUpdate.remove(i);
        i->updateVisual();
        auto p = dynamic_cast<AVGroupItem*>(i->parent());
        if (p != nullptr) {
            toUpdate.insert(p);
        }
    }
}

void AnnotationsTreeView::sl_onAnnotationsRemoved(const QList<Annotation*>& as) {
    TreeSorter ts(this);
    Q_UNUSED(ts);

    tree->disconnect(this, SIGNAL(sl_onItemSelectionChanged()));

    auto aObj = qobject_cast<AnnotationTableObject*>(sender());
    SAFE_POINT(aObj != nullptr, "Invalid annotation table detected!", );
    AVGroupItem* groupItem = findGroupItem(aObj->getRootGroup());
    QHash<AVGroupItem*, int> groups2RemovedCount;

    foreach (Annotation* a, as) {
        QList<AVAnnotationItem*> aItems;
        groupItem->findAnnotationItems(aItems, a);
        for (AVAnnotationItem* ai : qAsConst(aItems)) {
            selectedAnnotation.remove(ai);

            auto parentGroup = static_cast<AVGroupItem*>(ai->parent());
            if (groups2RemovedCount.contains(parentGroup)) {
                ++groups2RemovedCount[parentGroup];
            } else {
                groups2RemovedCount.insert(parentGroup, 1);
            }
            delete ai;
        }
    }
    foreach (AVGroupItem* g, groups2RemovedCount.keys()) {
        g->updateVisual(groups2RemovedCount[g]);
    }

    connect(tree, SIGNAL(itemSelectionChanged()), SLOT(sl_onItemSelectionChanged()));
    sl_onItemSelectionChanged();
}

void AnnotationsTreeView::sl_onAnnotationsModified(const QList<AnnotationModification>& annotationModifications) {
    foreach (const AnnotationModification& annotationModification, annotationModifications) {
        switch (annotationModification.type) {
            case AnnotationModification_NameChanged:
            case AnnotationModification_LocationChanged:
            case AnnotationModification_TypeChanged: {
                QList<AVAnnotationItem*> aItems = findAnnotationItems(annotationModification.annotation);
                assert(!aItems.isEmpty());
                for (AVAnnotationItem* ai : qAsConst(aItems)) {
                    ai->updateVisual(ATVAnnUpdateFlag_BaseColumns);
                }
            } break;

            case AnnotationModification_QualifierRemoved: {
                const QualifierModification& qm = static_cast<const QualifierModification&>(annotationModification);
                QList<AVAnnotationItem*> aItems = findAnnotationItems(qm.annotation);
                for (AVAnnotationItem* ai : qAsConst(aItems)) {
                    ai->removeQualifier(qm.getQualifier());
                }
            } break;
            case AnnotationModification_QualifierAdded: {
                const QualifierModification& qm = static_cast<const QualifierModification&>(annotationModification);
                QList<AVAnnotationItem*> aItems = findAnnotationItems(qm.annotation);
                for (AVAnnotationItem* ai : qAsConst(aItems)) {
                    if (ai->isExpanded() || ai->childCount() > 1) {  // if item was expanded - add real qualifier items
                        ai->addQualifier(qm.getQualifier());
                    } else {
                        ai->setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator);  // otherwise process indicator only
                    }
                }
            } break;
        }
    }
}

void AnnotationsTreeView::sl_onGroupCreated(AnnotationGroup* g) {
    CHECK(findGroupItem(g) == nullptr, );
    AVGroupItem* pg = g->getParentGroup() == nullptr ? nullptr : findGroupItem(g->getParentGroup());
    buildGroupTree(pg, g);
    pg->updateVisual();
}

void AnnotationsTreeView::sl_onGroupRemoved(AnnotationGroup* parent, AnnotationGroup* g) {
    AVGroupItem* pg = findGroupItem(parent);
    if (pg == nullptr) {
        return;
    }

    tree->disconnect(this, SIGNAL(sl_onItemSelectionChanged()));

    removeGroupAnnotationsFromCache(pg);

    for (int i = 0, n = pg->childCount(); i < n; i++) {
        auto item = static_cast<AVItem*>(pg->child(i));
        if (AVItemType_Group == item->type && (static_cast<AVGroupItem*>(item))->group == g) {
            if (item->parent() != nullptr) {
                item->parent()->removeChild(item);
            }
            delete item;
            break;
        }
    }

    pg->updateVisual();

    connect(tree, SIGNAL(itemSelectionChanged()), SLOT(sl_onItemSelectionChanged()));
}

void AnnotationsTreeView::sl_onGroupRenamed(AnnotationGroup* g) {
    AVGroupItem* gi = findGroupItem(g);
    SAFE_POINT(gi != nullptr, "Invalid view item detected!", );
    gi->updateVisual();
}

AVGroupItem* AnnotationsTreeView::buildGroupTree(AVGroupItem* parentGroupItem, AnnotationGroup* g, bool areAnnotationsNew) {
    auto groupItem = new AVGroupItem(this, parentGroupItem, g);
    const QList<AnnotationGroup*> subgroups = g->getSubgroups();
    foreach (AnnotationGroup* subgroup, subgroups) {
        buildGroupTree(groupItem, subgroup);
    }
    const QList<Annotation*> annotations = g->getAnnotations();
    foreach (Annotation* a, annotations) {
        buildAnnotationTree(groupItem, a, areAnnotationsNew);
    }
    groupItem->updateVisual();
    return groupItem;
}

AVAnnotationItem* AnnotationsTreeView::buildAnnotationTree(AVGroupItem* parentGroup, Annotation* a, bool areAnnotationsNew) {
    if (!areAnnotationsNew) {
        AVAnnotationItem* annotationItem = findAnnotationItem(parentGroup, a);
        CHECK(annotationItem == nullptr, annotationItem);
    }

    auto annotationItem = new AVAnnotationItem(parentGroup, a);
    const QVector<U2Qualifier> qualifiers = a->getQualifiers();
    if (!qualifiers.isEmpty()) {
        annotationItem->setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator);
    }
    return annotationItem;
}

void AnnotationsTreeView::populateAnnotationQualifiers(AVAnnotationItem* ai) {
    SAFE_POINT(QTreeWidgetItem::ShowIndicator == ai->childIndicatorPolicy(), "Invalid policy indicator detected!", );
    SAFE_POINT(0 == ai->childCount(), "Unexpected tree item's child count!", );
    const QVector<U2Qualifier> qualifiers = ai->annotation->getQualifiers();
    foreach (const U2Qualifier& q, qualifiers) {
        auto curQualifierItem = new AVQualifierItem(ai, q);
        Q_UNUSED(curQualifierItem);
    }
    ai->setChildIndicatorPolicy(QTreeWidgetItem::DontShowIndicatorWhenChildless);
}

class SettingsUpdater : public TreeWidgetVisitor {
public:
    SettingsUpdater(const QStringList& cs)
        : changedSettings(cs) {
    }

    bool isChildVisitRequired(QTreeWidgetItem* ti) override {
        auto item = static_cast<AVItem*>(ti);
        assert(item->type != AVItemType_Qualifier);
        return item->type == AVItemType_Group;  // visit children only for group items
    }

    void visit(QTreeWidgetItem* ti) override {
        auto item = static_cast<AVItem*>(ti);
        if (item->type == AVItemType_Group) {
            auto gi = static_cast<AVGroupItem*>(item);
            gi->updateVisual();
        } else if (item->type == AVItemType_Annotation) {
            auto ai = static_cast<AVAnnotationItem*>(item);
            QString name = ai->annotation->getName();
            if (changedSettings.contains(name)) {
                ai->updateVisual(ATVAnnUpdateFlag_BaseColumns);
            }
        }
    }

private:
    QStringList changedSettings;
};

void AnnotationsTreeView::sl_onAnnotationSettingsChanged(const QStringList& changedSettings) {
    // first clear colors cache for changed annotations
    QMap<QString, QIcon>& cache = AVAnnotationItem::getIconsCache();
    foreach (const QString& name, changedSettings) {
        cache.remove(name);
    }
    // second -> update all annotations
    SettingsUpdater su(changedSettings);
    TreeWidgetUtils::visitDFS(tree, &su);
}

void AnnotationsTreeView::updateColumnContextActions(AVItem* item, int col) {
    copyColumnTextAction->setEnabled(item != nullptr && (col >= 3 || (item->type == AVItemType_Annotation && col == 1)) && !item->text(col).isEmpty());
    copyColumnURLAction->setEnabled(item != nullptr && col >= 3 && item->isColumnLinked(col));
    if (!copyColumnTextAction->isEnabled()) {
        copyColumnTextAction->setText(tr("Copy column text"));
    } else {
        QString colName;
        if (col >= 3) {
            assert(qColumns.size() > col - 3);
            colName = qColumns[col - 3];
            copyColumnTextAction->setText(tr("Copy column '%1' text").arg(colName));
        } else {
            auto ai = static_cast<AVAnnotationItem*>(item);
            copyColumnTextAction->setText(tr("Copy '%1' annotation location").arg(ai->annotation->getName()));
        }
    }

    if (!copyColumnURLAction->isEnabled()) {
        copyColumnURLAction->setText(tr("copy column URL"));
    } else {
        assert(qColumns.size() > col - 3);
        QString colName = qColumns[col - 3];
        copyColumnURLAction->setText(tr("Copy column '%1' URL").arg(colName));
    }
}

void AnnotationsTreeView::sl_onBuildMenu(GObjectViewController*, QMenu* m, const QString& type) {
    if (type != GObjectViewMenuType::CONTEXT) {
        return;
    }
    // Add actions that not depends on the point popup is called
    adjustMenu(m);

    QPoint globalPos = QCursor::pos();
    QPoint treePos = tree->mapFromGlobal(globalPos);
    if (!tree->rect().contains(treePos)) {
        return;
    }

    // Check is popup is called for column header
    QHeaderView* header = tree->header();
    QPoint headerPoint = header->mapFromGlobal(globalPos);
    if (header->rect().contains(headerPoint)) {
        int idx = header->logicalIndexAt(headerPoint);
        if (idx >= 3) {
            assert(idx - 3 < qColumns.size());
            lastClickedColumn = idx;
            removeColumnByHeaderClickAction->setText(tr("Hide '%1' column").arg(qColumns[lastClickedColumn - 3]));
            QAction* first = m->actions().first();
            m->insertAction(first, removeColumnByHeaderClickAction);
            m->insertSeparator(first);
        }
        return;
    }

    // Ensure that item clicked is in the tree selection. Do not destroy multi-selection if present
    QList<QTreeWidgetItem*> selItems = tree->selectedItems();
    QPoint viewportPos = tree->viewport()->mapFromGlobal(globalPos);
    if (selItems.size() <= 1) {
        QTreeWidgetItem* item = tree->itemAt(viewportPos);
        if (item != nullptr) {
            if (selItems.size() == 1 && selItems.first() != item) {
                selItems.first()->setSelected(false);
            }
            item->setSelected(true);
        }
    }

    // Update column sensitive actions that appears only in context menu
    selItems = tree->selectedItems();
    lastClickedColumn = tree->columnAt(viewportPos.x());
    updateColumnContextActions(selItems.size() == 1 ? static_cast<AVItem*>(selItems.first()) : static_cast<AVItem*>(nullptr), lastClickedColumn);

    if (selItems.size() == 1) {
        auto avItem = static_cast<AVItem*>(selItems.first());
        AnnotationTableObject* aObj = avItem->getAnnotationTableObject();
        if (AutoAnnotationsSupport::isAutoAnnotationObject(aObj) && !aObj->getAnnotations().isEmpty()) {
            if (avItem->parent() != nullptr) {
                m->addAction(exportAutoAnnotationsGroup);
            }
        }
    }

    // Add active context actions to the top level menu
    QList<QAction*> contextActions;
    contextActions << toggleQualifierColumnAction;

    QList<QAction*> copySubmenuActions;
    copySubmenuActions << copyQualifierURLAction << copyColumnTextAction
                       << copyColumnURLAction;

    QMenu* copyMenu = GUIUtils::findSubMenu(m, ADV_MENU_COPY);
    SAFE_POINT(copyMenu != nullptr, "copyMenu", );
    foreach (QAction* a, copySubmenuActions) {
        if (a->isEnabled()) {
            copyMenu->addAction(a);
        }
    }
    int nActive = 0;
    QAction* first = m->actions().first();
    m->insertAction(first, searchQualifierAction);
    m->insertAction(first, invertAnnotationSelectionAction);

    QMenu* editMenu = GUIUtils::findSubMenu(m, ADV_MENU_EDIT);
    SAFE_POINT_NN(editMenu, );
    if (editAction->isEnabled()) {
        editMenu->addAction(editAction);
    }

    m->insertSeparator(first);
    foreach (QAction* a, contextActions) {
        if (a->isEnabled()) {
            nActive++;
            m->insertAction(first, a);
        }
    }
    if (nActive > 0) {
        m->insertSeparator(first);
    }
}

void AnnotationsTreeView::adjustMenu(QMenu* m) const {
    QMenu* addMenu = GUIUtils::findSubMenu(m, ADV_MENU_ADD);
    SAFE_POINT(addMenu != nullptr, "addMenu", );
    addMenu->addAction(addAnnotationObjectAction);
    addMenu->addAction(addQualifierAction);

    QMenu* removeMenu = GUIUtils::findSubMenu(m, ADV_MENU_REMOVE);
    SAFE_POINT(removeMenu != nullptr, "removeMenu", );
    removeMenu->addAction(removeObjectsFromViewAction);
    removeMenu->addAction(removeAnnsAndQsAction);
}

void AnnotationsTreeView::sl_paste() {
    PasteFactory* pasteFactory = AppContext::getPasteFactory();
    SAFE_POINT(pasteFactory != nullptr, "pasteFactory is null", );

    bool addToProject = !(tree && tree->hasFocus());
    PasteTask* task = pasteFactory->createPasteTask(addToProject);
    CHECK(task != nullptr, );
    if (!addToProject) {
        connect(new TaskSignalMapper(task), SIGNAL(si_taskFinished(Task*)), SLOT(sl_pasteFinished(Task*)));
    }
    AppContext::getTaskScheduler()->registerTopLevelTask(task);
}

void AnnotationsTreeView::sl_pasteFinished(Task* _pasteTask) {
    if (!tree || !tree->hasFocus()) {
        return;
    }

    auto pasteTask = qobject_cast<PasteTask*>(_pasteTask);
    if (pasteTask == nullptr || pasteTask->isCanceled()) {
        return;
    }
    const QList<Document*>& docs = pasteTask->getDocuments();
    if (docs.length() == 0) {
        return;
    }
    for (Document* doc : qAsConst(docs)) {
        foreach (GObject* annObj, doc->findGObjectByType(GObjectTypes::ANNOTATION_TABLE)) {
            ctx->tryAddObject(annObj);
        }
    }
}

void AnnotationsTreeView::sl_onAddAnnotationObjectToView() {
    ProjectTreeControllerModeSettings s;
    s.objectTypesToShow.insert(GObjectTypes::ANNOTATION_TABLE);
    s.groupMode = ProjectTreeGroupMode_ByDocument;
    foreach (GObject* o, ctx->getObjects()) {
        s.excludeObjectList.append(o);
    }
    QList<GObject*> objs = ProjectTreeItemSelectorDialog::selectObjects(s, this);
    foreach (GObject* obj, objs) {
        assert(obj->getGObjectType() == GObjectTypes::ANNOTATION_TABLE);
        ctx->tryAddObject(obj);
    }
}

static QList<AVGroupItem*> selectGroupItems(const QList<QTreeWidgetItem*>& items, TriState readOnly, TriState rootOnly) {
    QList<AVGroupItem*> res;
    foreach (QTreeWidgetItem* i, items) {
        auto item = static_cast<AVItem*>(i);
        if (item->type == AVItemType_Group) {
            auto gItem = static_cast<AVGroupItem*>(item);
            if (rootOnly != TriState_Unknown) {
                bool groupIsRoot = gItem->parent() == nullptr;
                if ((rootOnly == TriState_Yes && !groupIsRoot) || (rootOnly == TriState_No && groupIsRoot)) {
                    continue;
                }
            }
            if (readOnly != TriState_Unknown) {
                bool groupReadOnly = gItem->isReadonly();
                if ((readOnly == TriState_Yes && !groupReadOnly) || (readOnly == TriState_No && groupReadOnly)) {
                    continue;
                }
            }
            res.append(gItem);
        }
    }
    return res;
}

static QList<AVAnnotationItem*> selectAnnotationItems(const QList<QTreeWidgetItem*>& items, TriState readOnly) {
    QList<AVAnnotationItem*> res;
    foreach (QTreeWidgetItem* i, items) {
        auto item = static_cast<AVItem*>(i);
        if (item->type == AVItemType_Annotation) {
            auto aItem = static_cast<AVAnnotationItem*>(item);
            if (readOnly != TriState_Unknown) {
                bool aReadOnly = aItem->isReadonly();
                if ((readOnly == TriState_Yes && !aReadOnly) || (readOnly == TriState_No && aReadOnly)) {
                    continue;
                }
            }
            res.append(aItem);
        }
    }
    return res;
}

static QList<AVQualifierItem*> selectQualifierItems(const QList<QTreeWidgetItem*>& items, TriState readOnly) {
    QList<AVQualifierItem*> res;
    foreach (QTreeWidgetItem* i, items) {
        auto item = static_cast<AVItem*>(i);
        if (item->type == AVItemType_Qualifier) {
            auto qItem = static_cast<AVQualifierItem*>(item);
            if (readOnly != TriState_Unknown) {
                bool qReadOnly = qItem->isReadonly();
                if ((readOnly == TriState_Yes && !qReadOnly) || (readOnly == TriState_No && qReadOnly)) {
                    continue;
                }
            }
            res.append(qItem);
        }
    }
    return res;
}

void AnnotationsTreeView::sl_removeObjectFromView() {
    if (isDragging) {
        return;
    }

    QList<AVGroupItem*> topLevelGroups = selectGroupItems(tree->selectedItems(), TriState_Unknown, TriState_Yes);
    QList<GObject*> objects;
    for (AVGroupItem* gItem : qAsConst(topLevelGroups)) {
        objects.append(gItem->group->getGObject());
    }

    for (GObject* obj : qAsConst(objects)) {
        SAFE_POINT(obj->getGObjectType() == GObjectTypes::ANNOTATION_TABLE, "Unexpected object type", );
        if (AutoAnnotationsSupport::isAutoAnnotationObject(obj)) {
            continue;
        }
        auto sequenceObjects = ctx->getSequenceObjectsWithContexts();
        for (auto seqObj : qAsConst(sequenceObjects)) {
            obj->removeObjectRelation(GObjectRelation(GObjectReference(seqObj), ObjectRole_Sequence));
        }
    }
}

static bool groupDepthInverseComparator(const AVGroupItem* i1, const AVGroupItem* i2) {
    int depth1 = i1->group->getGroupDepth();
    int depth2 = i2->group->getGroupDepth();
    return depth1 > depth2;
}

void AnnotationsTreeView::sl_removeAnnsAndQs() {
    // remove selected qualifiers first (cache them, since different qualifier items with equal name/val are not distinguished)
    if (isDragging) {
        return;
    }

    QList<AVQualifierItem*> qualifierItemsToRemove = selectQualifierItems(tree->selectedItems(), TriState_No);
    QList<U2Qualifier> qualsToRemove;
    qualsToRemove.reserve(qualifierItemsToRemove.size());
    QList<Annotation*> qualAnnotations;
    qualAnnotations.reserve(qualifierItemsToRemove.size());

    for (int i = 0, n = qualifierItemsToRemove.size(); i < n; i++) {
        AVQualifierItem* qi = qualifierItemsToRemove[i];
        auto ai = static_cast<AVAnnotationItem*>(qi->parent());
        qualAnnotations << ai->annotation;
        qualsToRemove << U2Qualifier(qi->qName, qi->qValue);
    }
    for (int i = 0, n = qualifierItemsToRemove.size(); i < n; i++) {
        Annotation* a = qualAnnotations.at(i);
        const U2Qualifier& q = qualsToRemove.at(i);
        a->removeQualifier(q);
    }

    // remove selected annotations now
    QList<AVAnnotationItem*> annotationItemsToRemove = selectAnnotationItems(tree->selectedItems(), TriState_No);

    QMultiMap<AnnotationGroup*, Annotation*> annotationsByGroup;
    foreach (AVAnnotationItem* aItem, annotationItemsToRemove) {
        SAFE_POINT(!aItem->annotation->getGObject()->isStateLocked(), "Attempting to remove annotations from locked table!", );
        auto ag = (static_cast<AVGroupItem*>(aItem->parent())->group);
        annotationsByGroup.insert(ag, aItem->annotation);
    }

    QList<AnnotationGroup*> agroups = annotationsByGroup.uniqueKeys();
    foreach (AnnotationGroup* ag, agroups) {
        const QList<Annotation*> annotations = annotationsByGroup.values(ag);
        ag->removeAnnotations(annotations);
    }

    // now remove selected groups
    QList<AVGroupItem*> groupItemsToRemove = selectGroupItems(tree->selectedItems(), TriState_No, TriState_No);

    std::sort(groupItemsToRemove.begin(), groupItemsToRemove.end(), groupDepthInverseComparator);
    // now remove all groups
    foreach (AVGroupItem* gi, groupItemsToRemove) {
        AnnotationGroup* pg = gi->group->getParentGroup();
        pg->removeSubgroup(gi->group);
    }
}

void AnnotationsTreeView::updateState() {
    QList<QTreeWidgetItem*> items = tree->selectedItems();

    QList<AVGroupItem*> topLevelGroups = selectGroupItems(items, TriState_Unknown, TriState_Yes);

    bool hasAutoAnnotationObjects = false;
    foreach (AVGroupItem* item, topLevelGroups) {
        AnnotationTableObject* aObj = item->getAnnotationTableObject();
        if (AutoAnnotationsSupport::isAutoAnnotationObject(aObj)) {
            hasAutoAnnotationObjects = true;
            break;
        }
    }

    removeObjectsFromViewAction->setEnabled(!topLevelGroups.isEmpty() && !hasAutoAnnotationObjects);

    QList<AVGroupItem*> nonRootModGroups = selectGroupItems(items, TriState_No, TriState_No);
    QList<AVAnnotationItem*> modAnnotations = selectAnnotationItems(items, TriState_No);
    QList<AVQualifierItem*> modQualifiers = selectQualifierItems(items, TriState_No);
    removeAnnsAndQsAction->setEnabled(!nonRootModGroups.isEmpty() || !modAnnotations.isEmpty() || !modQualifiers.isEmpty());

    bool hasOnly1QualifierSelected = items.size() == 1 && (static_cast<AVItem*>(items.first()))->type == AVItemType_Qualifier;
    QString qName = hasOnly1QualifierSelected ? (static_cast<AVQualifierItem*>(items.first()))->qName : QString("");

    emit si_setCopyQualifierActionStatus(hasOnly1QualifierSelected, hasOnly1QualifierSelected ? tr("Copy qualifier '%1' value").arg(qName) : tr("Copy qualifier text"));

    bool hasOnly1QualifierSelectedWithURL = hasOnly1QualifierSelected && (static_cast<AVItem*>(items.first()))->isColumnLinked(1);
    copyQualifierURLAction->setEnabled(hasOnly1QualifierSelectedWithURL);
    copyQualifierURLAction->setText(hasOnly1QualifierSelectedWithURL ? tr("Copy qualifier '%1' URL").arg(qName) : tr("Copy qualifier URL"));

    toggleQualifierColumnAction->setEnabled(hasOnly1QualifierSelected);
    bool hasColumn = qColumns.contains(qName);
    toggleQualifierColumnAction->setText(!hasOnly1QualifierSelected ? tr("Toggle column") : (qColumns.contains(qName) ? tr("Hide '%1' column") : tr("Add '%1' column")).arg(qName));

    toggleQualifierColumnAction->setIcon(hasOnly1QualifierSelected ? (hasColumn ? removeColumnIcon : addColumnIcon) : QIcon());

    QTreeWidgetItem* ciBase = tree->currentItem();
    auto ci = static_cast<AVItem*>(ciBase);
    bool editableItemSelected = items.size() == 1 && ci != nullptr && ci == items.first() && !ci->isReadonly();
    editAction->setEnabled(editableItemSelected);

    bool hasEditableAnnotationContext = editableItemSelected && (ci->type == AVItemType_Annotation || ci->type == AVItemType_Qualifier);
    addQualifierAction->setEnabled(hasEditableAnnotationContext);
}

static bool isReadOnly(QTreeWidgetItem* item) {
    for (; item; item = item->parent()) {
        auto itemi = dynamic_cast<AVItem*>(item);
        AnnotationTableObject* obj;
        switch (itemi->type) {
            case AVItemType_Group:
                obj = dynamic_cast<AVGroupItem*>(itemi)->group->getGObject();
                break;
            case AVItemType_Annotation:
                obj = dynamic_cast<AVAnnotationItem*>(itemi)->annotation->getGObject();
                break;
            default:
                continue;
        }
        if (obj->isStateLocked()) {
            return true;
        }
    }
    return false;
}

void AnnotationsTreeView::resetDragAndDropData() {
    dndCopyOnly = false;
    dndSelItems.clear();
    dropDestination = nullptr;
}

bool AnnotationsTreeView::eventFilter(QObject* o, QEvent* e) {
    if (o != tree->viewport()) {
        return false;
    }
    const QEvent::Type etype = e->type();
    switch (etype) {
        case QEvent::ToolTip: {
            auto helpEvent = static_cast<const QHelpEvent*>(e);
            QPoint globalPos = helpEvent->globalPos();
            QPoint viewportPos = tree->viewport()->mapFromGlobal(globalPos);
            auto avItem = static_cast<const AVItem*>(tree->itemAt(viewportPos));
            if (avItem != nullptr && avItem->type == AVItemType_Annotation) {
                auto annotationItem = static_cast<const AVAnnotationItem*>(avItem);
                ADVSequenceObjectContext* sequenceContext = ctx->getSequenceContext(annotationItem->getAnnotationTableObject());
                bool isValidSequence = sequenceContext != nullptr;

                const SharedAnnotationData& annotationData = annotationItem->annotation->getData();
                int maxTextRows = 15;
                U2SequenceObject* sequenceObject = isValidSequence ? sequenceContext->getSequenceObject() : nullptr;
                DNATranslation* complTT = isValidSequence ? sequenceContext->getComplementTT() : nullptr;
                DNATranslation* aminoTT = isValidSequence ? sequenceContext->getAminoTT() : nullptr;
                QString tip = Annotation::getQualifiersTip(annotationData, maxTextRows, sequenceObject, complTT, aminoTT);
                if (!tip.isEmpty()) {
                    QToolTip::showText(helpEvent->globalPos(), tip);
                    return true;
                }
            }
            return false;
        }
        case QEvent::MouseButtonRelease:
            lastMB = ((QMouseEvent*)e)->button();
            isDragging = false;
            return false;
        case QEvent::MouseButtonPress: {
            auto me = dynamic_cast<QMouseEvent*>(e);
            if (me->modifiers() == Qt::NoModifier && me->button() == Qt::LeftButton) {
                QTreeWidgetItem* item = tree->itemAt(dragStartPos = me->pos());
                if (item != nullptr) {
                    AVItemType type = dynamic_cast<AVItem*>(item)->type;
                    if (type == AVItemType_Annotation || type == AVItemType_Group) {
                        if (!tree->selectedItems().contains(item)) {
                            tree->setCurrentItem(item);
                        }
                        isDragging = true;
                        return false;
                    }
                }
            }
            isDragging = false;
            return false;
        }
        case QEvent::MouseMove: {
            auto me = dynamic_cast<QMouseEvent*>(e);
            if (!(me->buttons() & Qt::LeftButton) || !isDragging) {
                return false;
            }
            if ((me->pos() - dragStartPos).manhattanLength() < QApplication::startDragDistance()) {
                return true;
            }
            initiateDragAndDrop(me);
            return true;
        }
        case QEvent::DragEnter: {
            auto de = dynamic_cast<QDragEnterEvent*>(e);
            if (de->mimeData()->hasFormat(annotationMimeType)) {
                de->acceptProposedAction();
                uiLog.trace("Drag enter event in Annotations view : format is OK");
                return true;
            }
            uiLog.trace("Drag enter event in Annotations view : unsupported mime format!");
            return false;
        }
        case QEvent::DragMove: {  // update current D&D state
            auto de = dynamic_cast<QDragMoveEvent*>(e);
            if (de->mimeData()->hasFormat(annotationMimeType)) {
                QTreeWidgetItem* item = tree->itemAt(de->pos());
                if (item == nullptr) {
                    de->ignore();
                    uiLog.trace("Drag event in Annotations view : invalid destination");
                    return true;
                }
                if (isReadOnly(item)) {
                    de->ignore();
                    uiLog.trace(QString("Drag event in Annotations view : destination is read-only: %1").arg(item->text(0)));
                    return true;
                }
                Qt::DropAction dndAction = (de->keyboardModifiers() & Qt::ShiftModifier) ? Qt::CopyAction : Qt::MoveAction;
                if (dndAction == Qt::MoveAction && dndCopyOnly) {
                    de->ignore();
                    uiLog.trace("Drag event in Annotations view : source is read-only, can't move");
                    return true;
                }

                for (auto itemi = dynamic_cast<AVItem*>(item); itemi != nullptr; itemi = dynamic_cast<AVItem*>(itemi->parent())) {
                    if (itemi->type == AVItemType_Group) {
                        for (int i = 0, s = dndSelItems.size(); i < s; ++i) {
                            AVItem* dndItem = dndSelItems[i];
                            if (dndItem == itemi) {  // do not allow drop group into itself
                                de->ignore();
                                uiLog.trace("Drag event in Annotations view : can't move group into itself!");
                                return true;
                            }
                        }
                    }
                }
                if (item->parent() == nullptr) {  // destination is root item
                    for (int i = 0, s = dndSelItems.size(); i < s; ++i) {
                        AVItem* dndItem = dndSelItems[i];
                        if (dndItem->type == AVItemType_Annotation) {
                            de->ignore();
                            // root group can't have annotations -> problem with store/load invariant..
                            uiLog.trace("Drag event in Annotations view : can't move annotations, destination group is not found!");
                            return true;
                        }
                    }
                }
                de->setDropAction(dndAction);
                de->accept();
                uiLog.trace(QString("Drag event in Annotations view, status: OK, type: %1").arg(dndAction == Qt::MoveAction ? "Move" : "Copy"));
                return true;
            }
            return false;
        }
        case QEvent::Drop: {
            uiLog.trace("Drop event in Annotations view");
            auto de = dynamic_cast<QDropEvent*>(e);
            const QMimeData* mime = de->mimeData();
            if (mime->hasFormat(annotationMimeType)) {
                QTreeWidgetItem* item = tree->itemAt(de->pos());
                if (item != nullptr) {
                    auto avItem = dynamic_cast<AVItem*>(item);
                    while (avItem != nullptr && avItem->type != AVItemType_Group) {
                        avItem = dynamic_cast<AVItem*>(avItem->parent());
                    }
                    if (avItem != nullptr) {
                        dropDestination = static_cast<AVGroupItem*>(avItem);
                        SAFE_POINT(dropDestination != nullptr, "dropDestination is NULL", false);
                        if (de->keyboardModifiers() & Qt::ShiftModifier) {
                            de->setDropAction(Qt::CopyAction);
                        } else {
                            de->setDropAction(Qt::MoveAction);
                        }
                        de->accept();
                        uiLog.trace(QString("Drop event in Annotations view : destination group %1").arg(dropDestination->group->getGroupPath()));
                    } else {
                        uiLog.trace("Drop event in Annotations view : something weird, can't derive destination group!");
                    }
                    return true;
                } else {
                    uiLog.trace("Drop event in Annotations view : invalid destination");
                }
            } else {
                uiLog.trace("Drop event in Annotations view : unknown MIME format");
                return false;
            }
            de->ignore();
            return true;
        }
        default:
            return false;
    }
}

bool AnnotationsTreeView::initiateDragAndDrop(QMouseEvent*) {
    uiLog.trace("Starting drag & drop in annotations view");

    resetDragAndDropData();

    // Now filter selection: keep only parent items in the items list, remove qualifiers, check if 'move' is allowed
    QList<QTreeWidgetItem*> initialSelItems = tree->selectedItems();  // whole selection will be moved
    dndCopyOnly = false;  // allow 'move' by default first
    for (int i = 0, n = initialSelItems.size(); i < n; i++) {
        auto itemi = dynamic_cast<AVItem*>(initialSelItems[i]);
        SAFE_POINT_NN(itemi, false);
        AnnotationTableObject* ao = itemi->getAnnotationTableObject();
        SAFE_POINT_NN(ao, false);
        if (AutoAnnotationsSupport::isAutoAnnotationObject(ao)) {
            //  only allow to drag top-level auto annotations groups
            if (!(itemi->type == AVItemType_Group && itemi->parent() != nullptr)) {
                continue;
            }
        }
        if (!dndCopyOnly && isReadOnly(itemi)) {
            dndCopyOnly = true;
        }
        if (itemi->type == AVItemType_Annotation) {
            // if annotation group is in the selection -> remove annotation from the list, it will be moved/copied automatically with group
            bool addItem = true;
            for (QTreeWidgetItem* cur = itemi->parent(); cur != nullptr; cur = cur->parent()) {
                if (initialSelItems.contains(cur)) {
                    addItem = false;
                    break;
                }
            }
            if (addItem) {
                dndSelItems.append(itemi);
            }
        } else if (itemi->type == AVItemType_Qualifier) {  // remove qualifiers from selection
            itemi->setSelected(false);
        } else {
            SAFE_POINT(itemi->type == AVItemType_Group, "An unexpected tree item type", false);
            if (itemi->parent() == nullptr) {  // object level group -> add all subgroups
                for (int j = 0, m = itemi->childCount(); j < m; j++) {
                    auto citem = dynamic_cast<AVItem*>(itemi->child(j));
                    SAFE_POINT(citem->type == AVItemType_Group || citem->type == AVItemType_Annotation, "An unexpected child tree item type", false);
                    dndSelItems.append(citem);
                }
            } else {
                dndSelItems.append(itemi);
            }
        }
    }
    if (dndSelItems.isEmpty()) {
        resetDragAndDropData();
        uiLog.trace("No items to drag & drop");
        return false;
    }

    // serialize selected items : // TODO: actually we do not need it today, so skipping this step
    // QStringList annotationsAndGroupsRefs;
    QByteArray serializedReferences;
    serializedReferences.append(serializedReferences);

    // initiate dragging
    auto mimeData = new QMimeData();
    mimeData->setData(annotationMimeType, serializedReferences);
    auto drag = new QDrag(this);
    drag->setMimeData(mimeData);
    Qt::DropAction dndAction = drag->exec(Qt::MoveAction | Qt::CopyAction, Qt::CopyAction);
    if (dndAction == Qt::MoveAction || dndAction == Qt::CopyAction) {
        finishDragAndDrop(dndAction);
    } else {
        uiLog.trace("Drag & drop in Annotations view was ignored");
    }
    resetDragAndDropData();
    return true;
}

typedef QPair<AnnotationGroup*, QString> GroupChangeInfo;
typedef QPair<GroupChangeInfo, Annotation*> AnnotationDnDInfo;

static void collectAnnotationDnDInfo(AnnotationGroup* ag, const QString& destGroupPath, QList<AnnotationDnDInfo>& annotationsToProcess) {
    foreach (Annotation* a, ag->getAnnotations()) {
        annotationsToProcess << AnnotationDnDInfo(GroupChangeInfo(ag, destGroupPath), a);
    }
    const QString newDestGroupPath = destGroupPath + "/" + ag->getName();
    foreach (AnnotationGroup* sag, ag->getSubgroups()) {
        collectAnnotationDnDInfo(sag, newDestGroupPath, annotationsToProcess);
    }
}

void AnnotationsTreeView::finishDragAndDrop(Qt::DropAction dndAction) {
    SAFE_POINT(dropDestination != nullptr, "dropDestination is NULL", );
    AnnotationTableObject* dstObject = dropDestination->getAnnotationTableObject();

    // Can not drag anything to auto-annotation object
    if (AutoAnnotationsSupport::isAutoAnnotationObject(dstObject)) {
        return;
    }

    const QString destGroupPath = dropDestination->group->getGroupPath();

    QList<AnnotationGroup*> affectedGroups;
    QList<AnnotationDnDInfo> affectedAnnotations;
    QList<Task*> moveAutoAnnotationTasks;
    QStringList manualCreationGroups;

    for (int i = 0, n = dndSelItems.size(); i < n; ++i) {
        AVItem* selItem = dndSelItems.at(i);
        SAFE_POINT(selItem->parent() != nullptr, "Invalid tree item parent", );  // we never have no top-level object items in dndSelItems
        if (selItem->type == AVItemType_Annotation) {
            auto fromGroupItem = dynamic_cast<AVGroupItem*>(selItem->parent());
            auto ai = dynamic_cast<AVAnnotationItem*>(selItem);
            affectedAnnotations << AnnotationDnDInfo(GroupChangeInfo(fromGroupItem->group, destGroupPath), ai->annotation);
        } else {
            auto movedGroupItem = dynamic_cast<AVGroupItem*>(selItem);
            if (movedGroupItem->group->getParentGroup() == dropDestination->group) {
                continue;  // can't drop group into itself
            }

            // auto-annotations have to be handled differently
            if (AutoAnnotationsSupport::isAutoAnnotationObject(movedGroupItem->getAnnotationTableObject())) {
                GObjectReference dstRef(dstObject);
                ADVSequenceObjectContext* seqCtx = ctx->getActiveSequenceContext();
                Task* t = new ExportAutoAnnotationsGroupTask(movedGroupItem->getAnnotationGroup(), dstRef, seqCtx);
                moveAutoAnnotationTasks.append(t);
                continue;
            }
            QString toGroupPath = destGroupPath + (destGroupPath.isEmpty() ? "" : "/") + movedGroupItem->group->getName();
            if (!movedGroupItem->group->hasAnnotations()) {
                // this group will not be created as part of annotation move process -> create it manually
                manualCreationGroups.append(toGroupPath);
            }
            collectAnnotationDnDInfo(movedGroupItem->group, toGroupPath, affectedAnnotations);
            affectedGroups.append(movedGroupItem->group);
        }
    }

    uiLog.trace(QString("Finishing drag & drop in Annotations view, affected groups: %1 , top-level annotations: %2")
                    .arg(affectedGroups.size())
                    .arg(affectedAnnotations.size()));

    // Move or Copy annotation reference inside of the object
    dndAdded.clear();
    QList<Annotation*> dndToRemove;  // used to remove dragged annotations at once in case of Qt::MoveAction (see below)
    QList<AnnotationGroup*> srcGroupList;  // used to remove dragged annotations at once in case of Qt::MoveAction (see below)
    QList<AnnotationGroup*> dstGroupList;  // used to add all dragged annotations at once (see below)
    foreach (const AnnotationDnDInfo& adnd, affectedAnnotations) {
        const QString& toGroupPath = adnd.first.second;
        AnnotationGroup* dstGroup = dstObject->getRootGroup()->getSubgroup(toGroupPath, true);
        if (dstGroup == dstObject->getRootGroup()) {  // root group can't have annotations -> problem with store/load invariant..
            continue;
        }
        AnnotationGroup* srcGroup = adnd.first.first;
        Annotation* srcAnnotation = adnd.second;
        Annotation* dstAnnotation = srcAnnotation;
        if (srcAnnotation->getGObject() != dstObject) {
            dstAnnotation = dstGroup->addAnnotations(QList<SharedAnnotationData>() << srcAnnotation->getData()).first();
        }
        bool doAdd = !dstGroup->getAnnotations().contains(dstAnnotation);
        bool doRemove = dndAction == Qt::MoveAction && doAdd;
        if (doAdd) {
            dndAdded.append(dstAnnotation);
            dstGroupList.append(dstGroup);
        }
        if (doRemove) {
            srcGroupList.append(srcGroup);
            dndToRemove.append(srcAnnotation);
        }
    }

    // Add and remove the dragged annotations to the receiver AnnotationsTreeView at once.
    // It is required for the case of cross-view drag and drop.
    int i = 0;
    for (AnnotationGroup* g : qAsConst(dstGroupList)) {
        g->addAnnotations({dndAdded.at(i)->getData()});
        i++;
    }

    for (Annotation* annotation : qAsConst(dndToRemove)) {
        annotation->getGroup()->removeAnnotations({annotation});
    }

    // Process groups
    if (dndAction == Qt::MoveAction) {
        foreach (AnnotationGroup* ag, affectedGroups) {
            ag->getParentGroup()->removeSubgroup(ag);
        }
    }
    // manually create empty group items
    foreach (const QString& path, manualCreationGroups) {
        dstObject->getRootGroup()->getSubgroup(path, true);
    }

    // make auto-annotations persistent
    foreach (Task* t, moveAutoAnnotationTasks) {
        AppContext::getTaskScheduler()->registerTopLevelTask(t);
    }
}

void AnnotationsTreeView::sl_itemEntered(QTreeWidgetItem* i, int column) {
    auto item = static_cast<AVItem*>(i);
    Qt::CursorShape newShape = Qt::ArrowCursor;
    Qt::CursorShape currentShape = tree->cursor().shape();
    if (item != nullptr) {
        if (item->isColumnLinked(column)) {
            newShape = Qt::PointingHandCursor;
        }
    }
    if (newShape == Qt::PointingHandCursor || ((newShape == Qt::ArrowCursor && currentShape == Qt::PointingHandCursor))) {
        tree->setCursor(newShape);
    }
}

void AnnotationsTreeView::sl_itemDoubleClicked(QTreeWidgetItem* i, int) {
    auto item = static_cast<AVItem*>(i);

    if (item->type == AVItemType_Annotation) {
        auto ai = dynamic_cast<AVAnnotationItem*>(item);

        QVector<U2Region> annotationRegions = ai->annotation->getRegions();
        SAFE_POINT(!annotationRegions.isEmpty(), "Annotation regions are empty", );

        annotationDoubleClicked(ai, annotationRegions.toList());
    }

    if (item->type == AVItemType_Qualifier) {
        auto qi = static_cast<AVQualifierItem*>(item);
        editQualifierItem(qi);
    }
}

void AnnotationsTreeView::sl_itemClicked(QTreeWidgetItem* i, int column) {
    auto item = static_cast<AVItem*>(i);
    if (lastMB != Qt::LeftButton || item == nullptr || !item->isColumnLinked(column)) {
        return;
    }

    QString fileUrl = item->getFileUrl(column);
    if (!fileUrl.isEmpty()) {
        Task* task = new LoadRemoteDocumentAndAddToProjectTask(fileUrl);
        AppContext::getTaskScheduler()->registerTopLevelTask(task);
    } else {
        GUIUtils::runWebBrowser(item->buildLinkURL(column));
    }
}

void AnnotationsTreeView::sl_itemPressed(QTreeWidgetItem* i) {
    auto item = static_cast<AVItem*>(i);

    if (item->type == AVItemType_Annotation) {
        auto ai = dynamic_cast<AVAnnotationItem*>(item);
        QMap<AVAnnotationItem*, QList<U2Region>> sortedAnnotationSelections = sortAnnotationSelection(QList<AnnotationTableObject*>() << ai->getAnnotationTableObject());
        annotationClicked(ai, sortedAnnotationSelections);
    }
}

void AnnotationsTreeView::sl_itemExpanded(QTreeWidgetItem* qi) {
    auto i = static_cast<AVItem*>(qi);
    if (i->type != AVItemType_Annotation) {
        return;
    }
    auto ai = static_cast<AVAnnotationItem*>(i);
    if (ai->childCount() == 0 && !ai->annotation->getQualifiers().isEmpty()) {
        SAFE_POINT(ai->childIndicatorPolicy() == QTreeWidgetItem::ShowIndicator, "Unexpected tree child indicator policy", );
        populateAnnotationQualifiers(ai);
        ai->updateVisual(ATVAnnUpdateFlag_BaseColumns);
    } else {
        SAFE_POINT(ai->childIndicatorPolicy() == QTreeWidgetItem::DontShowIndicatorWhenChildless, "Unexpected tree child indicator policy", );
    }
}

void AnnotationsTreeView::sl_annotationActivated(Annotation* annotation, int regionIndex) {
    Q_UNUSED(regionIndex);
    sl_annotationClicked(annotation);
}

// TODO: refactor this method
// UTI-155
void AnnotationsTreeView::sl_annotationClicked(Annotation* annotation) {
    AnnotationSelection* annotationSelection = ctx->getAnnotationsSelection();

    const QList<AVAnnotationItem*> annotationItems = findAnnotationItems(annotation);
    CHECK(annotationItems.size() == 1, );
    AVAnnotationItem* item = annotationItems.first();

    auto advctx = qobject_cast<ADVSequenceObjectContext*>(sender());
    SAFE_POINT(advctx != nullptr, "Incorrect sender", );

    QList<AnnotationTableObject*> annotationObjects = toList(advctx->getAnnotationObjects());
    QMap<AVAnnotationItem*, QList<U2Region>> sortedAnnotationSelections = sortAnnotationSelection(annotationObjects);

    expandItemRecursevly(item->parent());

    annotationSelection->add(item->annotation);
    annotationClicked(item, sortedAnnotationSelections, item->annotation->getRegions().toList());
}

// TODO: refactor this method
// UTI-155
// See review of UGENE-5936 for details
void AnnotationsTreeView::sl_annotationDoubleClicked(Annotation* annotation, int regionIndex) {
    QList<U2Region> annotationRegions = annotation->getRegions().toList();
    SAFE_POINT(0 <= regionIndex && regionIndex < annotationRegions.size(), "Unexpected region index", );

    const U2Region& indexRegion = annotationRegions[regionIndex];
    QList<U2Region> regionsToSelect = {indexRegion};
    AnnotationTableObject* tableObject = annotation->getGObject();
    SAFE_POINT(tableObject != nullptr, "AnnotationTableObject isn't found", );

    ADVSequenceObjectContext* sequenceContext = ctx->getSequenceContext(tableObject);
    SAFE_POINT(sequenceContext != nullptr, "ADVSequenceObjectContext isn't found", );

    U2SequenceObject* sequenceObject = sequenceContext->getSequenceObject();
    SAFE_POINT(sequenceObject != nullptr, "U2SequenceObject isn't found", );

    const int sequenceLength = sequenceObject->getSequenceLength();
    if (sequenceObject->isCircular() && (indexRegion.startPos == 0 || indexRegion.endPos() == sequenceLength)) {
        foreach (const U2Region& reg, annotationRegions) {
            CHECK_CONTINUE(reg != annotationRegions[regionIndex]);
            CHECK_CONTINUE(reg.startPos == 0 || reg.endPos() == sequenceLength);

            regionsToSelect.append(reg);
            break;
        }
    }
    QList<AVAnnotationItem*> annotationItems = findAnnotationItems(annotation);
    foreach (AVAnnotationItem* item, annotationItems) {
        expandItemRecursevly(item->parent());
        {
            QSignalBlocker blocker(tree);
            item->setSelected(true);
        }
        annotationDoubleClicked(item, regionsToSelect);
    }
}

void AnnotationsTreeView::expandItemRecursevly(QTreeWidgetItem* item) {
    CHECK(item != nullptr, );
    QTreeWidgetItem* parent = item->parent();
    if (parent != nullptr) {
        expandItemRecursevly(parent);
    }
    tree->expandItem(item);
}

// In case of several sequences, we need to sort only selections of the current sequence
QMap<AVAnnotationItem*, QList<U2Region>> AnnotationsTreeView::sortAnnotationSelection(QList<AnnotationTableObject*> annotationObjects) {
    QMap<AVAnnotationItem*, QList<U2Region>> sortedAnnotation;
    foreach (AVAnnotationItem* key, selectedAnnotation.keys()) {
        AnnotationTableObject* tableObject = key->getAnnotationTableObject();
        if (annotationObjects.contains(tableObject)) {
            sortedAnnotation.insert(key, selectedAnnotation[key]);
        }
    }

    return sortedAnnotation;
}

void AnnotationsTreeView::sl_clearSelectedAnnotations() {
    auto advctx = qobject_cast<ADVSequenceObjectContext*>(sender());
    SAFE_POINT(advctx != nullptr, "Incorrect sender", );

    QList<AnnotationTableObject*> annotationObjects = toList(advctx->getAnnotationObjects());
    QMap<AVAnnotationItem*, QList<U2Region>> currentAnnotationSelections = sortAnnotationSelection(annotationObjects);

    foreach (AVAnnotationItem* key, currentAnnotationSelections.keys()) {
        selectedAnnotation.remove(key);
    }
}

void AnnotationsTreeView::sl_sequenceAdded(ADVSequenceObjectContext* advContext) {
    connectSequenceObjectContext(advContext);
}

void AnnotationsTreeView::sl_sequenceRemoved(ADVSequenceObjectContext* advContext) {
    disconnectSequenceObjectContext(advContext);
}

// TODO: refactoring of annotationClicked and annotationDoubleClicked methods.
// It's too difficult to understand what's going on in this methods
// UTI-155
void AnnotationsTreeView::annotationClicked(AVAnnotationItem* item, QMap<AVAnnotationItem*, QList<U2Region>> selectedAnnotations, const QList<U2Region>& selectedRegions) {
    ADVSequenceObjectContext* seqObjCtx = ctx->getSequenceContext(item->getAnnotationTableObject());
    SAFE_POINT(seqObjCtx != nullptr, "ADVSequenceObjectContext is NULL", );
    emitAnnotationActivated(item->annotation);

    DNASequenceSelection* sequenceSelection = seqObjCtx->getSequenceSelection();
    SAFE_POINT(sequenceSelection != nullptr, "DNASequenceSelection is NULL", );

    Qt::KeyboardModifiers km = QApplication::keyboardModifiers();
    const bool controlOfShiftPressed = km.testFlag(Qt::ControlModifier) || km.testFlag(Qt::ShiftModifier);
    if (!controlOfShiftPressed) {
        if (sequenceSelection->getSelectedRegions().toList() != item->annotation->getRegions().toList()) {
            sequenceSelection->clear();
        }
        foreach (AVAnnotationItem* key, selectedAnnotations.keys()) {
            selectedAnnotation.remove(key);
        }
    } else {
        QVector<U2Region> toSelect;
        if (!selectedRegions.isEmpty() &&
            selectedAnnotations.value(item).contains(selectedRegions.first()) &&
            selectedAnnotations.value(item).contains(selectedRegions.last())) {
            selectedAnnotation[item].removeOne(selectedRegions.first());
            selectedAnnotations[item].removeOne(selectedRegions.first());
            if (selectedRegions.size() == 2) {
                selectedAnnotation[item].removeOne(selectedRegions.last());
                selectedAnnotations[item].removeOne(selectedRegions.last());
            }
            if (selectedAnnotation[item].isEmpty()) {
                selectedAnnotation.remove(item);
            }
        }
        foreach (AVAnnotationItem* annItem, selectedAnnotations.keys()) {
            QList<U2Region> regions = selectedAnnotations.value(annItem);
            for (U2Region newRegion : qAsConst(regions)) {
                for (const U2Region& toSel : qAsConst(toSelect)) {
                    if (toSel.intersects(newRegion)) {
                        newRegion = U2Region::containingRegion(newRegion, toSel);
                        toSelect.removeOne(toSel);
                    }
                }
                toSelect << newRegion;
            }
        }
        sequenceSelection->clear();
        sequenceSelection->setSelectedRegions(toSelect);
    }
}

void AnnotationsTreeView::annotationDoubleClicked(AVAnnotationItem* item, const QList<U2Region>& selectedRegions) {
    selectedAnnotation[item] << selectedRegions;

    ADVSequenceObjectContext* seqObjCtx = ctx->getSequenceContext(item->getAnnotationTableObject());
    SAFE_POINT(seqObjCtx != nullptr, "ADVSequenceObjectContext is NULL", );
    emitAnnotationActivated(item->annotation);

    DNASequenceSelection* sequenceSelection = seqObjCtx->getSequenceSelection();
    SAFE_POINT(sequenceSelection != nullptr, "DNASequenceSelection is NULL", );

    AnnotationSelection* annotationSelection = seqObjCtx->getAnnotationsSelection();
    SAFE_POINT(annotationSelection != nullptr, "AnnotationSelection is NULL", );

    annotationSelection->add(item->annotation);

    QList<U2Region> regionsToSelect = selectedRegions;
    const QVector<U2Region> regions = sequenceSelection->getSelectedRegions();
    for (const U2Region& reg : qAsConst(regions)) {
        for (const U2Region& selectedRegion : qAsConst(selectedRegions)) {
            if (reg.intersects(selectedRegion)) {
                sequenceSelection->removeRegion(reg);
                regionsToSelect.removeOne(selectedRegion);
                regionsToSelect << U2Region::containingRegion(reg, selectedRegion);
            }
        }
    }
    foreach (const U2Region& reg, regionsToSelect) {
        sequenceSelection->addRegion(reg);
    }
}

void AnnotationsTreeView::emitAnnotationActivated(Annotation* annotation) {
    ADVSequenceObjectContext* seqObjCtx = ctx->getSequenceContext(annotation->getGObject());
    disconnectSequenceObjectContext(seqObjCtx);
    seqObjCtx->emitAnnotationActivated(annotation, -1);
    connectSequenceObjectContext(seqObjCtx);
}

void AnnotationsTreeView::clearSelectedNotAnnotations() {
    foreach (QTreeWidgetItem* item, tree->selectedItems()) {
        auto aVItem = static_cast<AVItem*>(item);
        if (aVItem != nullptr && aVItem->type != AVItemType_Annotation) {
            item->setSelected(false);
        }
    }
}

void AnnotationsTreeView::sl_onCopyQualifierValue() {
    QList<QTreeWidgetItem*> items = tree->selectedItems();
    assert(items.size() == 1);
    auto item = static_cast<AVItem*>(items.first());
    assert(item->type == AVItemType_Qualifier);
    auto qi = static_cast<AVQualifierItem*>(item);
    QApplication::clipboard()->setText(qi->qValue);
}

void AnnotationsTreeView::sl_onCopyQualifierURL() {
    QList<QTreeWidgetItem*> items = tree->selectedItems();
    assert(items.size() == 1);
    auto item = static_cast<AVItem*>(items.first());
    if (item->isColumnLinked(1)) {
        QApplication::clipboard()->setText(item->buildLinkURL(1));
    }
}

void AnnotationsTreeView::sl_onCopyColumnText() {
    QList<QTreeWidgetItem*> items = tree->selectedItems();
    assert(items.size() == 1);
    auto item = static_cast<AVItem*>(items.first());
    QApplication::clipboard()->setText(item->text(lastClickedColumn));
}

void AnnotationsTreeView::sl_onCopyColumnURL() {
    QList<QTreeWidgetItem*> items = tree->selectedItems();
    assert(items.size() == 1);
    auto item = static_cast<AVItem*>(items.first());
    QApplication::clipboard()->setText(item->buildLinkURL(lastClickedColumn));
}

void AnnotationsTreeView::sl_onToggleQualifierColumn() {
    QList<QTreeWidgetItem*> items = tree->selectedItems();
    assert(items.size() == 1);
    auto item = static_cast<AVItem*>(items.first());
    assert(item->type == AVItemType_Qualifier);
    auto qi = static_cast<AVQualifierItem*>(item);
    if (qColumns.contains(qi->qName)) {
        removeQualifierColumn(qi->qName);
    } else {
        addQualifierColumn(qi->qName);
    }
}

void AnnotationsTreeView::sl_onRemoveColumnByHeaderClick() {
    if (isDragging) {
        return;
    }

    assert(lastClickedColumn >= 3);
    assert(lastClickedColumn - 3 <= qColumns.size());
    removeQualifierColumn(qColumns[lastClickedColumn - 3]);
}

void AnnotationsTreeView::sl_searchQualifier() {
    SAFE_POINT(currentItem() != nullptr,
               "Qualifier search cannot be started: no annotation tree item is selected for searching in", )
    QObjectScopedPointer<SearchQualifierDialog> d = new SearchQualifierDialog(this, this);
    d->exec();
}

void AnnotationsTreeView::sl_invertSelection() {
    QItemSelectionModel* selectionModel = tree->selectionModel();
    QItemSelection originalSelection = selectionModel->selection();
    QItemSelection unselectedAnnotations;
    QItemSelection selectedAnnotations;

    QStack<QModelIndex> stack;
    stack.push(tree->rootIndex());

    QAbstractItemModel* itemModel = tree->model();
    while (!stack.isEmpty()) {
        const QModelIndex parent = stack.pop();
        auto aItem = dynamic_cast<AVAnnotationItem*>(tree->itemFromIndex(parent));
        if (aItem != nullptr) {
            if (originalSelection.contains(parent)) {
                selectedAnnotations.select(parent, parent);
            } else {
                unselectedAnnotations.select(parent, parent);
            }
        }

        const int childCount = itemModel->rowCount(parent);
        for (int i = 0; i < childCount; ++i) {
            const QModelIndex child = itemModel->index(i, 0, parent);
            if (child.isValid()) {
                stack.push(child);
            }
        }
    }
    selectionModel->select(selectedAnnotations, QItemSelectionModel::Deselect | QItemSelectionModel::Rows);
    selectionModel->select(unselectedAnnotations, QItemSelectionModel::Select | QItemSelectionModel::Rows);
}

void AnnotationsTreeView::updateAllAnnotations(ATVAnnUpdateFlags flags) {
    QString emptyFilter;
    for (int i = 0; i < tree->topLevelItemCount(); i++) {
        auto gi = static_cast<AVGroupItem*>(tree->topLevelItem(i));
        gi->updateAnnotations(emptyFilter, flags);
    }
}

void AnnotationsTreeView::addQualifierColumn(const QString& q) {
    TreeSorter ts(this);

    qColumns.append(q);
    int nColumns = headerLabels.size() + qColumns.size();
    tree->setColumnCount(nColumns);
    tree->setHeaderLabels(headerLabels + qColumns);
    tree->setColumnWidth(nColumns - 2, nColumns - 3 == 1 ? 200 : 100);
    updateAllAnnotations(ATVAnnUpdateFlag_QualColumns);

    updateState();
}

void AnnotationsTreeView::removeQualifierColumn(const QString& q) {
    bool ok = qColumns.removeOne(q);
    if (!ok) {
        return;
    }

    TreeSorter ts(this);

    tree->setColumnCount(headerLabels.size() + qColumns.size());
    tree->setHeaderLabels(headerLabels + qColumns);
    updateAllAnnotations(ATVAnnUpdateFlag_QualColumns);

    updateState();
}

#define COLUMN_NAMES "ATV_COLUMNS"

void AnnotationsTreeView::saveState(QVariantMap& map) const {
    map.insert(COLUMN_NAMES, QVariant(qColumns));

    QStringList columns = map.value(COLUMN_NAMES).toStringList();
    assert(columns == qColumns);
}

void AnnotationsTreeView::updateState(const QVariantMap& map) {
    QStringList columns = map.value(COLUMN_NAMES).toStringList();

    if (columns != qColumns && !columns.isEmpty()) {
        TreeSorter ts(this);
        foreach (QString q, qColumns) {
            removeQualifierColumn(q);
        }
        foreach (QString q, columns) {
            addQualifierColumn(q);
        }
    }
}

void AnnotationsTreeView::setSortingEnabled(bool v) {
    if (sortTimer.isActive()) {
        sortTimer.stop();
    }
    if (v) {
        sortTimer.start();
    } else {
        tree->setSortingEnabled(false);
    }
}

void AnnotationsTreeView::sl_sortTree() {
    tree->setSortingEnabled(true);
}

void AnnotationsTreeView::sl_edit() {
    auto item = static_cast<AVItem*>(tree->currentItem());
    editItem(item);
}

void AnnotationsTreeView::editGroupItem(AVGroupItem* gi) {
    SAFE_POINT(gi != nullptr, "Item is NULL", );
    SAFE_POINT(gi->group->getParentGroup() != nullptr, "Attempting renaming of root group!", );  // not a root group
    QString oldName = gi->group->getName();
    QString newName = renameDialogHelper(gi, oldName, tr("Edit Group"));
    AnnotationGroup* parentAnnotationGroup = gi->group->getParentGroup();
    if (newName != oldName && AnnotationGroup::isValidGroupName(newName, false) && parentAnnotationGroup->getSubgroup(newName, false) == nullptr) {
        gi->group->setName(newName);
        gi->group->getGObject()->setModified(true);
        gi->updateVisual();
    }
}

void AnnotationsTreeView::editQualifierItem(AVQualifierItem* qi) {
    SAFE_POINT(qi != nullptr, "Item is NULL", );
    auto ai = static_cast<AVAnnotationItem*>(qi->parent());
    U2Qualifier q;
    bool ro = qi->isReadonly();
    bool ok = editQualifierDialogHelper(qi, ro, q);
    QString simplifiedValue = AVQualifierItem::simplifyText(q.value);
    if (!ro && ok && (q.name != qi->qName || simplifiedValue != qi->qValue)) {
        auto a = (static_cast<AVAnnotationItem*>(qi->parent()))->annotation;
        a->removeQualifier(U2Qualifier(qi->qName, qi->qValue));
        a->addQualifier(q);
        AVQualifierItem* newQualifierItem = ai->findQualifierItem(q.name, q.value);
        tree->setCurrentItem(newQualifierItem);
        tree->scrollToItem(newQualifierItem);
    }
}

void AnnotationsTreeView::editAnnotationItem(AVAnnotationItem* ai) {
    QVector<U2Region> l = ai->annotation->getRegions();
    QList<ADVSequenceObjectContext*> soList = ctx->findRelatedSequenceContexts(ai->annotation->getGObject());
    SAFE_POINT(1 == soList.size(), "Invalid sequence context count!", );
    ADVSequenceObjectContext* so = soList.first();

    CreateAnnotationModel m;
    m.sequenceObjectRef = GObjectReference(so->getSequenceGObject());
    m.defaultIsNewDoc = false;
    m.groupName = ai->getAnnotationGroup()->getName();
    m.data = ai->annotation->getData();
    QString oldDescription = ai->annotation->findFirstQualifierValue(GBFeatureUtils::QUALIFIER_NOTE);
    m.description = oldDescription;
    m.hideAnnotationTableOption = true;
    m.hideGroupName = true;
    m.useAminoAnnotationTypes = so->getAminoTT() == nullptr;

    m.annotationObjectRef = GObjectReference(ai->getAnnotationTableObject());
    m.sequenceLen = so->getSequenceLength();
    QObjectScopedPointer<CreateAnnotationDialog> dlg = new CreateAnnotationDialog(this, m);
    QString helpPageId = "17467567";  //! the line is important for the script that changes the documentation links
    dlg->updateAppearance(tr("Edit Annotation"), helpPageId, tr("Edit"));
    const int result = dlg->exec();

    if (result == QDialog::Accepted) {
        QString newName = m.data->name;
        if (newName != ai->annotation->getName()) {
            ai->annotation->setName(newName);
            QList<AVAnnotationItem*> ais = findAnnotationItems(ai->annotation);
            foreach (AVAnnotationItem* a, ais) {
                a->updateVisual(ATVAnnUpdateFlag_BaseColumns);
            }
        }
        U2Location location = m.data->location;
        if (!location->regions.isEmpty() && l != location->regions) {
            ai->annotation->updateRegions(location->regions);
        }

        U2FeatureType newType = m.data->type;
        if (newType != ai->annotation->getType()) {
            ai->annotation->setType(newType);
        }

        if ((m.description.isEmpty() && oldDescription != QString()) || (m.description != oldDescription)) {
            ai->annotation->removeQualifier(U2Qualifier(GBFeatureUtils::QUALIFIER_NOTE, oldDescription));
        }
        if (!m.description.isEmpty() && m.description != oldDescription) {
            ai->annotation->addQualifier(U2Qualifier(GBFeatureUtils::QUALIFIER_NOTE, m.description));
        }

        ai->annotation->setLocationOperator(location->op);
        ai->annotation->setStrand(location->strand);
        ai->annotation->getGObject()->setModified(true);
    }
}

void AnnotationsTreeView::moveDialogToItem(QTreeWidgetItem* item, QDialog* d) {
    if (item == nullptr) {
        return;
    }
    tree->scrollToItem(item);

    // try place dialog right below or right above the item
    d->layout()->update();
    QRect itemRect = tree->visualItemRect(item).translated(tree->viewport()->mapToGlobal(QPoint(0, 0)));
    QSize dialogSize = d->layout()->minimumSize();
    QRect dialogRect(0, 0, dialogSize.width(), dialogSize.height() + 35);  //+35 -> estimation for a title bar
    QRect widgetRect = rect().translated(mapToGlobal(QPoint(0, 0)));
    QRect finalDialogRect = dialogRect.translated(itemRect.bottomLeft());
    if (!widgetRect.contains(finalDialogRect)) {
        finalDialogRect = dialogRect.translated(itemRect.topLeft()).translated(QPoint(0, -dialogRect.height()));
    }
    if (widgetRect.contains(finalDialogRect)) {
        d->move(finalDialogRect.topLeft());
    }
}

QString AnnotationsTreeView::renameDialogHelper(AVItem* i, const QString& defText, const QString& title) {
    QObjectScopedPointer<QDialog> d = new QDialog(this);
    d->setWindowTitle(title);
    auto l = new QVBoxLayout();
    d->setLayout(l);

    auto edit = new QLineEdit(d.data());
    edit->setText(defText);
    edit->setSelection(0, defText.length());
    connect(edit, SIGNAL(returnPressed()), d.data(), SLOT(accept()));
    l->addWidget(edit);

    moveDialogToItem(i, d.data());

    const int rc = d->exec();
    CHECK(!d.isNull(), "");

    if (rc == QDialog::Rejected) {
        return defText;
    }
    return edit->text();
}

bool AnnotationsTreeView::editQualifierDialogHelper(AVQualifierItem* i, bool ro, U2Qualifier& q) {
    QObjectScopedPointer<EditQualifierDialog> d = new EditQualifierDialog(this, U2Qualifier(i == nullptr ? "new_qualifier" : i->qName, i == nullptr ? "" : i->qValue), ro, i != nullptr);
    moveDialogToItem(i == nullptr ? tree->currentItem() : i, d.data());
    const int rc = d->exec();
    CHECK(!d.isNull(), false);
    q = d->getModifiedQualifier();
    return rc == QDialog::Accepted;
}

void AnnotationsTreeView::editItem(AVItem* item) {
    if (item->isReadonly()) {
        return;
    }
    if (item->type == AVItemType_Group) {
        auto gi = static_cast<AVGroupItem*>(item);
        editGroupItem(gi);
    } else if (item->type == AVItemType_Annotation) {
        auto ai = static_cast<AVAnnotationItem*>(item);
        if (ai->annotation->getData()->type == U2FeatureType::Comment) {
            QMessageBox::warning(this, L10N::warningTitle(), tr("Editing of \"comment\" annotation is not allowed!"));
            return;
        }
        editAnnotationItem(ai);
    } else {
        SAFE_POINT(item->type == AVItemType_Qualifier, "Unexpected annotation view item's qualifier!", );
        auto qi = static_cast<AVQualifierItem*>(item);
        editQualifierItem(qi);
    }
}

void AnnotationsTreeView::sl_addQualifier() {
    auto item = static_cast<AVItem*>(tree->currentItem());
    if (item->isReadonly() || item->type == AVItemType_Group) {
        return;
    }
    U2Qualifier q;
    bool ok = editQualifierDialogHelper(nullptr, false, q);
    if (ok) {
        assert(!q.name.isEmpty());
        auto ai = item->type == AVItemType_Annotation ? static_cast<AVAnnotationItem*>(item) : static_cast<AVAnnotationItem*>(item->parent());
        Annotation* a = ai->annotation;
        a->addQualifier(q);
        ai->setExpanded(true);
        AVQualifierItem* qi = ai->findQualifierItem(q.name, q.value);
        tree->setCurrentItem(qi);
        tree->scrollToItem(qi);
    }
}

void AnnotationsTreeView::sl_annotationObjectModifiedStateChanged() {
    auto ao = qobject_cast<AnnotationTableObject*>(sender());
    SAFE_POINT(ao != nullptr, "Invalid annotation table!", );
    AVGroupItem* gi = findGroupItem(ao->getRootGroup());
    SAFE_POINT(gi != nullptr, "Invalid annotation view item detected!", );
    gi->updateVisual();
}

AVItem* AnnotationsTreeView::currentItem() {
    return static_cast<AVItem*>(tree->currentItem());
}

void AnnotationsTreeView::sl_exportAutoAnnotationsGroup() {
    auto item = static_cast<AVItem*>(tree->currentItem());
    AnnotationGroup* ag = item->getAnnotationGroup();

    ADVSequenceObjectContext* seqCtx = ctx->getActiveSequenceContext();

    CreateAnnotationModel m;
    m.hideAnnotationType = true;
    m.hideAnnotationName = true;
    m.hideLocation = true;
    m.groupName = ag->getName();
    m.sequenceObjectRef = GObjectReference(seqCtx->getSequenceObject());

    QObjectScopedPointer<CreateAnnotationDialog> dlg = new CreateAnnotationDialog(this, m, "65929453");
    dlg->setWindowTitle(tr("Create Permanent Annotation"));
    dlg->exec();
    CHECK(!dlg.isNull() && dlg->result() == QDialog::Accepted, );

    auto task = new ExportAutoAnnotationsGroupTask(ag, m.annotationObjectRef, seqCtx, m.description);
    AppContext::getTaskScheduler()->registerTopLevelTask(task);
}

//////////////////////////////////////////////////////////////////////////
/// Tree model

AVItem::AVItem(QTreeWidgetItem* parent, AVItemType _type)
    : QTreeWidgetItem(parent), type(_type) {
}

bool AVItem::isReadonly() const {
    SAFE_POINT(parent() != nullptr, "Invalid annotation parent item!", false);
    return (static_cast<AVItem*>(parent())->isReadonly());
}

AnnotationsTreeView* AVItem::getAnnotationTreeView() const {
    SAFE_POINT(parent() != nullptr, "Invalid annotation parent item!", nullptr);
    return (static_cast<AVItem*>(parent())->getAnnotationTreeView());
}

AnnotationTableObject* AVItem::getAnnotationTableObject() const {
    SAFE_POINT(parent() != nullptr, "Invalid annotation parent item!", nullptr);
    return (static_cast<AVItem*>(parent())->getAnnotationTableObject());
}

AnnotationGroup* AVItem::getAnnotationGroup() {
    SAFE_POINT(parent() != nullptr, "Invalid annotation parent item!", getAnnotationTableObject()->getRootGroup());
    return (static_cast<AVItem*>(parent())->getAnnotationGroup());
}

bool AVItem::processLinks(const QString& qName, const QString& qValue, int col) {
    bool linked = false;
    if (qName == "db_xref") {
        QStringList l = qValue.split(":");
        QString dbName = l[0];
        DBXRefInfo info = AppContext::getDBXRefRegistry()->getRefByKey(dbName);
        linked = !info.url.isEmpty();
        setToolTip(col, info.comment);
        if (linked) {
            setData(col, Qt::UserRole, true);
        }
    }

    if (linked) {
        QFont f = font(col);
        f.setUnderline(true);
        setFont(col, f);
        setForeground(col, Qt::blue);
    }
    return linked;
}

bool AVItem::isColumnLinked(int col) const {
    return data(col, Qt::UserRole).type() == QVariant::Bool;
}

QString AVItem::buildLinkURL(int col) const {
    assert(isColumnLinked(col));
    QString qValue = text(col);
    QStringList split = qValue.split(":");
    QString type = split.first();
    QString id = split.size() < 2 ? QString("") : split[1];
    QString url = AppContext::getDBXRefRegistry()->getRefByKey(type).url.arg(id);
    return url;
}

QString AVItem::getFileUrl(int col) const {
    assert(isColumnLinked(col));
    QStringList split = text(col).split(":");
    QString type = split.first();
    QString fileUrl = AppContext::getDBXRefRegistry()->getRefByKey(type).fileUrl;
    if (!fileUrl.isEmpty()) {
        QString id = split.size() < 2 ? QString("") : split[1];
        return fileUrl.arg(id);
    }
    return fileUrl;
}

AVGroupItem::AVGroupItem(AnnotationsTreeView* atv, AVGroupItem* parent, AnnotationGroup* g)
    : AVItem(parent, AVItemType_Group), group(g), atv(atv) {
    updateVisual();
}

const QIcon& AVGroupItem::getGroupIcon() {
    static QIcon groupIcon(":/core/images/group_green_active.png");
    return groupIcon;
}

const QIcon& AVGroupItem::getDocumentIcon() {
    static QIcon groupIcon(":/core/images/gobject.png");
    return groupIcon;
}

void AVGroupItem::updateVisual(int removedAnnotationCount) {
    SAFE_POINT(0 <= removedAnnotationCount, "Invalid removed item count!", )
    if (parent() == nullptr) {  // document item
        AnnotationTableObject* aobj = group->getGObject();
        Document* doc = aobj->getDocument();
        QString text = aobj->getGObjectName();
        if (doc != nullptr) {
            const QString docShortName = aobj->getDocument()->getName();
            SAFE_POINT(!docShortName.isEmpty(), "Invalid document name detected!", );
            text = group->getGObject()->getGObjectName() + " [" + docShortName + "]";
            if (aobj->isTreeItemModified()) {
                text += " *";
            }
        }
        setText(AnnotationsTreeView::COLUMN_NAME, text);
        setIcon(AnnotationsTreeView::COLUMN_NAME, getDocumentIcon());
        GUIUtils::setMutedLnF(this, !aobj->hasAnnotations(), false);
    } else {  // usual groups with annotations
        int na = group->getAnnotations().size() - removedAnnotationCount;
        int ng = group->getSubgroups().size();
        const QString nameString = group->getName() + "  " + QString("(%1, %2)").arg(ng).arg(na);
        setText(AnnotationsTreeView::COLUMN_NAME, nameString);
        setIcon(AnnotationsTreeView::COLUMN_NAME, getGroupIcon());

        // if all child items are muted -> mute this group too
        bool showDisabled = childCount() > 0;  // empty group is not disabled
        for (int i = 0; i < childCount(); i++) {
            QTreeWidgetItem* childItem = child(i);
            if (!GUIUtils::isMutedLnF(childItem)) {
                showDisabled = false;
                break;
            }
        }
        GUIUtils::setMutedLnF(this, showDisabled, false);
    }
}

void AVGroupItem::updateAnnotations(const QString& nameFilter, ATVAnnUpdateFlags f) {
    bool noFilter = nameFilter.isEmpty();
    for (int j = 0; j < childCount(); j++) {
        auto item = static_cast<AVItem*>(child(j));
        if (item->type == AVItemType_Group) {
            auto level1 = static_cast<AVGroupItem*>(item);
            if (noFilter || level1->group->getName() == nameFilter) {
                level1->updateAnnotations(nameFilter, f);
            }
        } else {
            SAFE_POINT(item->type == AVItemType_Annotation, "Unexpected tree item type", );
            auto aItem = static_cast<AVAnnotationItem*>(item);
            if (noFilter || aItem->annotation->getName() == nameFilter) {
                aItem->updateVisual(f);
            }
        }
    }
}

bool AVGroupItem::isReadonly() const {
    // documents names are not editable
    GObject* obj = group->getGObject();
    bool readOnly = obj->isStateLocked() || AutoAnnotationsSupport::isAutoAnnotationObject(obj);
    return group->getParentGroup() == nullptr || readOnly;
}

void AVGroupItem::findAnnotationItems(QList<AVAnnotationItem*>& result, Annotation* a) const {
    for (int i = 0, n = childCount(); i < n; i++) {
        auto item = static_cast<AVItem*>(child(i));
        if (item->type == AVItemType_Group) {
            auto gi = static_cast<AVGroupItem*>(item);
            gi->findAnnotationItems(result, a);
        } else if (item->type == AVItemType_Annotation) {
            auto ai = static_cast<AVAnnotationItem*>(item);
            if (ai->annotation == a) {
                result.append(ai);
            }
        }
    }
}

AnnotationTableObject* AVGroupItem::getAnnotationTableObject() const {
    return group->getGObject();
}

AnnotationGroup* AVGroupItem::getAnnotationGroup() {
    return group;
}

AVAnnotationItem::AVAnnotationItem(AVGroupItem* parent, Annotation* a)
    : AVItem(parent, AVItemType_Annotation), annotation(a) {
    updateVisual(ATVAnnUpdateFlags(ATVAnnUpdateFlag_BaseColumns | ATVAnnUpdateFlag_QualColumns));
    hasNumericQColumns = false;
}

#define MAX_ICONS_CACHE_SIZE 500

QMap<QString, QIcon>& AVAnnotationItem::getIconsCache() {
    static QMap<QString, QIcon> iconsCache;
    return iconsCache;
}

void AVAnnotationItem::updateVisual(ATVAnnUpdateFlags f) {
    const SharedAnnotationData& aData = annotation->getData();
    AnnotationSettingsRegistry* registry = AppContext::getAnnotationsSettingsRegistry();
    const AnnotationSettings* as = registry->getAnnotationSettings(aData);

    if (f.testFlag(ATVAnnUpdateFlag_BaseColumns)) {
        QMap<QString, QIcon>& cache = getIconsCache();
        QIcon icon = cache.value(aData->name);
        if (icon.isNull()) {
            const QColor iconColor = as->visible ? as->color : Qt::lightGray;
            icon = GUIUtils::createSquareIcon(iconColor, 9);
            if (cache.size() > MAX_ICONS_CACHE_SIZE) {
                cache.clear();
            }
            cache[aData->name] = icon;
        }
        SAFE_POINT(!icon.isNull(), "Unable to initialize icon for annotation tree item!", );

        setIcon(AnnotationsTreeView::COLUMN_NAME, icon);
        setText(AnnotationsTreeView::COLUMN_NAME, aData->name);
        locationString = U1AnnotationUtils::buildLocationString(aData);
        setText(AnnotationsTreeView::COLUMN_TYPE, U2FeatureTypes::getVisualName(aData->type));
        setText(AnnotationsTreeView::COLUMN_VALUE, locationString);
    }

    if (f.testFlag(ATVAnnUpdateFlag_QualColumns)) {
        // setup custom qualifiers columns
        AnnotationsTreeView* atv = getAnnotationTreeView();
        SAFE_POINT(atv != nullptr, "Invalid annotations tree view!", );
        const QStringList& colNames = atv->getQualifierColumnNames();
        hasNumericQColumns = false;
        for (int i = 0, n = colNames.size(); i < n; i++) {
            const int col = 3 + i;
            const QString colName = colNames[i];
            const QString colText = aData->findFirstQualifierValue(colName);
            setText(3 + i, colText);
            const bool linked = processLinks(colName, colText, col);
            if (!linked) {
                bool ok = false;
                double d = colText.toDouble(&ok);
                if (ok) {
                    setData(col, Qt::UserRole, d);
                    hasNumericQColumns = true;
                }
            }
        }
    }

    GUIUtils::setMutedLnF(this, !as->visible, true);
}

QVariant AVAnnotationItem::data(int col, int role) const {
    if (col == AnnotationsTreeView::COLUMN_VALUE && role == Qt::DisplayRole) {
        if (locationString.isEmpty()) {
            const SharedAnnotationData& data = annotation->getData();
            locationString = U1AnnotationUtils::buildLocationString(data);
        }
        return locationString;
    }

    return QTreeWidgetItem::data(col, role);
}

bool AVAnnotationItem::operator<(const QTreeWidgetItem& other) const {
    int col = treeWidget()->sortColumn();
    const AVItem& avItem = static_cast<const AVItem&>(other);
    if (avItem.type != AVItemType_Annotation) {
        return text(col) < other.text(col);
    }
    const AVAnnotationItem& ai = static_cast<const AVAnnotationItem&>(other);
    if (col == AnnotationsTreeView::COLUMN_NAME) {
        const SharedAnnotationData& aData1 = annotation->getData();
        const SharedAnnotationData& aData2 = ai.annotation->getData();
        if (aData1->name == aData2->name) {
            // for annotations with equal names we compare locations
            // this allows one to avoid resorting on lazy qualifier loading
            return aData1->location->regions[0] < aData2->location->regions[0];
        }
        return aData1->name < aData2->name;
    }
    if (col == AnnotationsTreeView::COLUMN_VALUE || (isColumnNumeric(col) && ai.isColumnNumeric(col))) {
        double oval = ai.getNumericVal(col);
        double mval = getNumericVal(col);
        return mval < oval;
    }
    return text(col) < other.text(col);
}

bool AVAnnotationItem::isColumnNumeric(int col) const {
    if (col == AnnotationsTreeView::COLUMN_NAME) {
        return false;
    }
    if (col == AnnotationsTreeView::COLUMN_VALUE) {
        return true;
    }
    if (!hasNumericQColumns) {
        return false;
    }
    return data(col, Qt::UserRole).type() == QVariant::Double;
}

double AVAnnotationItem::getNumericVal(int col) const {
    if (col == AnnotationsTreeView::COLUMN_VALUE) {
        const U2Region& r = annotation->getLocation()->regions[0];
        return r.startPos;
    }
    bool ok = false;
    double d = data(col, Qt::UserRole).toDouble(&ok);
    assert(ok);
    return d;
}

void AVAnnotationItem::removeQualifier(const U2Qualifier& q) {
    AVQualifierItem* qi = findQualifierItem(q.name, q.value);
    CHECK(qi != nullptr, );
    delete qi;

    updateVisual(ATVAnnUpdateFlag_QualColumns);
}

void AVAnnotationItem::addQualifier(const U2Qualifier& q) {
    auto qi = new AVQualifierItem(this, q);
    Q_UNUSED(qi);
    updateVisual(ATVAnnUpdateFlag_QualColumns);
}

AVQualifierItem* AVAnnotationItem::findQualifierItem(const QString& name, const QString& val) const {
    QString simplifiedValue = AVQualifierItem::simplifyText(val);
    for (int i = 0, n = childCount(); i < n; i++) {
        auto qi = static_cast<AVQualifierItem*>(child(i));
        if (qi->qName == name && qi->qValue == simplifiedValue) {
            return qi;
        }
    }
    return nullptr;
}

AVQualifierItem::AVQualifierItem(AVAnnotationItem* parent, const U2Qualifier& q)
    : AVItem(parent, AVItemType_Qualifier), qName(q.name), qValue(q.value) {
    setText(AnnotationsTreeView::COLUMN_NAME, qName);
    qValue = simplifyText(qValue);
    setText(AnnotationsTreeView::COLUMN_VALUE, qValue);

    processLinks(qName, qValue, AnnotationsTreeView::COLUMN_VALUE);
}

QString AVQualifierItem::simplifyText(const QString& origValue) {
    QString res = origValue;
    res.replace("\t", "    ");
    res.replace("\r", "");
    res.replace("\n", " ");
    res = res.trimmed();
    return res;
}

}  // namespace U2
