// For license of this file, see <project-root-folder>/LICENSE.md.

#include "gui/feedsview.h"

#include "core/feedsmodel.h"
#include "core/feedsproxymodel.h"
#include "definitions/definitions.h"
#include "gui/dialogs/formmain.h"
#include "gui/messagebox.h"
#include "gui/reusable/styleditemdelegate.h"
#include "gui/reusable/treeviewcolumnsmenu.h"
#include "miscellaneous/feedreader.h"
#include "miscellaneous/mutex.h"
#include "miscellaneous/settings.h"
#include "miscellaneous/textfactory.h"
#include "qtlinq/qtlinq.h"
#include "services/abstract/feed.h"
#include "services/abstract/gui/formaccountdetails.h"
#include "services/abstract/rootitem.h"
#include "services/abstract/serviceroot.h"

#include <algorithm>

#include <QClipboard>
#include <QContextMenuEvent>
#include <QHeaderView>
#include <QMenu>
#include <QPainter>
#include <QPointer>
#include <QScrollBar>
#include <QTimer>

FeedsView::FeedsView(QWidget* parent)
  : BaseTreeView(parent), m_dontSaveExpandState(false),
    m_delegate(new StyledItemDelegate(qApp->settings()->value(GROUP(GUI), SETTING(GUI::HeightRowFeeds)).toInt(),
                                      -1,
                                      this)),
    m_columnsAdjusted(false), m_ignoreItemSelectionChange(false) {
  setObjectName(QSL("FeedsView"));

  // Allocate models.
  m_sourceModel = qApp->feedReader()->feedsModel();
  m_proxyModel = qApp->feedReader()->feedsProxyModel();

  // Connections.
  connect(header(), &QHeaderView::geometriesChanged, this, &FeedsView::adjustColumns);
  connect(&m_expansionDelayer, &QTimer::timeout, this, &FeedsView::reloadDelayedExpansions);
  connect(m_sourceModel, &FeedsModel::itemExpandRequested, this, &FeedsView::onItemExpandRequested);
  connect(header(), &QHeaderView::sortIndicatorChanged, this, &FeedsView::saveSortState);
  connect(m_proxyModel,
          &FeedsProxyModel::requireItemValidationAfterDragDrop,
          this,
          &FeedsView::validateItemAfterDragDrop);
  connect(m_proxyModel, &FeedsProxyModel::indexNotFilteredOutAnymore, this, &FeedsView::reloadItemExpandState);
  connect(this, &FeedsView::expanded, this, &FeedsView::onIndexExpanded);
  connect(this, &FeedsView::collapsed, this, &FeedsView::onIndexCollapsed);

  header()->setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu);
  connect(header(), &QHeaderView::customContextMenuRequested, this, [=](QPoint point) {
    TreeViewColumnsMenu mm(header());
    mm.exec(header()->mapToGlobal(point));
  });

  setModel(m_proxyModel);
  setupAppearance();
}

FeedsView::~FeedsView() {
  qDebugNN << LOGSEC_GUI << "Destroying FeedsView instance.";
}

void FeedsView::reloadFontSettings() {
  m_sourceModel->setupFonts();
}

void FeedsView::setSortingEnabled(bool enable) {
  disconnect(header(), &QHeaderView::sortIndicatorChanged, this, &FeedsView::saveSortState);
  QTreeView::setSortingEnabled(enable);
  connect(header(), &QHeaderView::sortIndicatorChanged, this, &FeedsView::saveSortState);
}

QList<Feed*> FeedsView::selectedFeeds(bool recursive) const {
  return qlinq::from(selectedItems())
    .selectMany([recursive](RootItem* it) {
      return it->getSubTreeFeeds(recursive);
    })
    .distinct()
    .toList();
}

RootItem* FeedsView::selectedItem() const {
  const QModelIndexList selected_rows = selectionModel()->selectedRows();
  const QModelIndex current_row = currentIndex();

  if (selected_rows.isEmpty()) {
    return nullptr;
  }
  else {
    RootItem* selected_item = m_sourceModel->itemForIndex(m_proxyModel->mapToSource(selected_rows.at(0)));

    if (selected_rows.size() == 1) {
      return selected_item;
    }

    auto selected_items = qlinq::from(selected_rows).select([this](const QModelIndex& idx) {
      return m_sourceModel->itemForIndex(m_proxyModel->mapToSource(idx));
    });

    RootItem* current_item = m_sourceModel->itemForIndex(m_proxyModel->mapToSource(current_row));

    if (std::find(selected_items.begin(), selected_items.end(), current_item) != selected_items.end()) {
      return current_item;
    }
    else {
      return selected_items.first();
    }
  }
}

QList<RootItem*> FeedsView::selectedItems() const {
  const QModelIndexList selected_rows = selectionModel()->selectedRows();

  return qlinq::from(selected_rows)
    .select([this](const QModelIndex& idx) {
      return m_sourceModel->itemForIndex(m_proxyModel->mapToSource(idx));
    })
    .toList();
}

void FeedsView::copyUrlOfSelectedFeeds() const {
  auto feeds = selectedFeeds(true);
  QStringList urls;

  for (const auto* feed : std::as_const(feeds)) {
    if (!feed->source().isEmpty()) {
      urls << feed->source();
    }
  }

  if (QGuiApplication::clipboard() != nullptr && !urls.isEmpty()) {
    QGuiApplication::clipboard()->setText(urls.join(TextFactory::newline()), QClipboard::Mode::Clipboard);
  }
}

void FeedsView::sortByColumn(int column, Qt::SortOrder order) {
  const int old_column = header()->sortIndicatorSection();
  const Qt::SortOrder old_order = header()->sortIndicatorOrder();

  if (column == old_column && order == old_order) {
    m_proxyModel->sort(column, order);
  }
  else {
    QTreeView::sortByColumn(column, order);
  }
}

void FeedsView::addFeedIntoSelectedAccount() {
  RootItem* selected = selectedItem();

  if (selected != nullptr) {
    ServiceRoot* root = selected->account();

    if (root->supportsFeedAdding()) {
      root->addNewFeed(selected, QGuiApplication::clipboard()->text(QClipboard::Mode::Clipboard));
    }
    else {
      qApp->showGuiMessage(Notification::Event::GeneralEvent,
                           {tr("Not supported by account"),
                            tr("Selected account does not support adding of new feeds."),
                            QSystemTrayIcon::MessageIcon::Warning});
    }
  }
}

void FeedsView::addCategoryIntoSelectedAccount() {
  RootItem* selected = selectedItem();

  if (selected != nullptr) {
    ServiceRoot* root = selected->account();

    if (root->supportsCategoryAdding()) {
      root->addNewCategory(selected);
    }
    else {
      qApp->showGuiMessage(Notification::Event::GeneralEvent,
                           {tr("Not supported by account"),
                            tr("Selected account does not support adding of new folders."),
                            QSystemTrayIcon::MessageIcon::Warning});
    }
  }
}

void FeedsView::expandCollapseCurrentItem(bool recursive) {
  if (selectionModel()->selectedRows().size() == 1) {
    QModelIndex index = selectionModel()->selectedRows().at(0);

    if (!model()->index(0, 0, index).isValid() && index.parent().isValid()) {
      setCurrentIndex(index.parent());
      index = index.parent();
    }

    if (recursive) {
      QList<QModelIndex> to_process = {index};
      bool expa = !isExpanded(index);

      while (!to_process.isEmpty()) {
        auto idx = to_process.takeFirst();

        if (idx.isValid()) {
          setExpanded(idx, expa);

          for (int i = 0; i < m_proxyModel->rowCount(idx); i++) {
            auto new_idx = m_proxyModel->index(i, 0, idx);

            if (new_idx.isValid()) {
              to_process << new_idx;
            }
          }
        }
        else {
          break;
        }
      }
    }
    else {
      isExpanded(index) ? collapse(index) : expand(index);
    }
  }
}

void FeedsView::updateSelectedItems() {
  auto sel_feeds = selectedFeeds(true);
  auto sel_items = selectedItems();
  bool one_specific_feed_selected = sel_items.size() == 1 && sel_items.constFirst()->kind() == RootItem::Kind::Feed;

  qApp->feedReader()->updateFeeds(sel_feeds, one_specific_feed_selected);
}

void FeedsView::clearSelectedItems() {
  if (MsgBox::show({},
                   QMessageBox::Icon::Question,
                   tr("Are you sure?"),
                   tr("Do you really want to clean all articles from selected items?"),
                   {},
                   {},
                   QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No,
                   QMessageBox::StandardButton::Yes,
                   QSL("clear_selected_feeds")) != QMessageBox::StandardButton::Yes) {
    return;
  }

  try {
    for (auto* it : selectedItems()) {
      m_sourceModel->markItemCleared(it, false);
    }
  }
  catch (const ApplicationException& ex) {
    qCriticalNN << LOGSEC_CORE << "Cannot clear items:" << NONQUOTE_W_SPACE_DOT(ex.message());
    qApp->showGuiMessage(Notification::Event::GeneralEvent,
                         GuiMessage(tr("Cannot clear items"),
                                    tr("Failed to clear items: %1.").arg(ex.message()),
                                    QSystemTrayIcon::MessageIcon::Critical),
                         GuiMessageDestination(true, true));
  }
}

void FeedsView::purgeSelectedFeeds() {
  if (MsgBox::show({},
                   QMessageBox::Icon::Question,
                   tr("Are you sure?"),
                   tr("Do you really want to purge all non-starred articles from selected feeds?"),
                   {},
                   {},
                   QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No,
                   QMessageBox::StandardButton::Yes,
                   QSL("purge_selected_feeds")) != QMessageBox::StandardButton::Yes) {
    return;
  }

  try {
    m_sourceModel->purgeArticles(selectedFeeds(true));
  }
  catch (const ApplicationException& ex) {
    qCriticalNN << LOGSEC_CORE << "Cannot purge feeds:" << NONQUOTE_W_SPACE_DOT(ex.message());
    qApp->showGuiMessage(Notification::Event::GeneralEvent,
                         GuiMessage(tr("Cannot purge feeds"),
                                    tr("Failed to purge feeds: %1.").arg(ex.message()),
                                    QSystemTrayIcon::MessageIcon::Critical),
                         GuiMessageDestination(true, true));
  }
}

void FeedsView::enableDisableSelectedFeeds() {
  auto feeds = selectedFeeds(true);

  if (feeds.isEmpty()) {
    return;
  }

  auto resp = feeds.size() == 1 ? QMessageBox::StandardButton::Yes
                                : MsgBox::show({},
                                               QMessageBox::Icon::Question,
                                               tr("Enable or disable feeds"),
                                               tr("You selected multiple feeds to enable/disable them."),
                                               tr("Do you really want to enable or disable selected feeds?"),
                                               {},
                                               QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No,
                                               QMessageBox::StandardButton::Yes,
                                               QSL("enable_disable_feeds"));

  if (resp != QMessageBox::StandardButton::Yes) {
    return;
  }

  for (Feed* feed : std::as_const(feeds)) {
    feed->setIsSwitchedOff(!feed->isSwitchedOff());

    qApp->database()->worker()->write([&](const QSqlDatabase& db) {
      DatabaseQueries::createOverwriteFeed(db, feed, feed->account()->accountId(), feed->parent()->id());
    });
  }

  m_sourceModel->reloadWholeLayout();
}

void FeedsView::clearAllItems() {
  if (MsgBox::show({},
                   QMessageBox::Icon::Question,
                   tr("Are you sure?"),
                   tr("Do you really want to clean all articles from selected items?"),
                   {},
                   {},
                   QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No,
                   QMessageBox::StandardButton::Yes,
                   QSL("clear_all_feeds")) != QMessageBox::StandardButton::Yes) {
    return;
  }

  try {
    m_sourceModel->markItemCleared(m_sourceModel->rootItem(), false);
  }
  catch (const ApplicationException& ex) {
    qCriticalNN << LOGSEC_CORE << "Cannot clear items:" << NONQUOTE_W_SPACE_DOT(ex.message());
    qApp->showGuiMessage(Notification::Event::GeneralEvent,
                         GuiMessage(tr("Cannot clear items"),
                                    tr("Failed to clear items: %1.").arg(ex.message()),
                                    QSystemTrayIcon::MessageIcon::Critical),
                         GuiMessageDestination(true, true));
  }
}

void FeedsView::editItems(const QList<RootItem*>& items) {
  if (!qApp->feedUpdateLock()->tryLock()) {
    // Lock was not obtained because
    // it is used probably by feed updater or application
    // is quitting.
    qApp->showGuiMessage(Notification::Event::GeneralEvent,
                         {tr("Cannot edit item"),
                          tr("Selected item cannot be edited because another critical operation is ongoing."),
                          QSystemTrayIcon::MessageIcon::Warning});

    // Thus, cannot delete and quit the method.
    return;
  }

  if (items.isEmpty()) {
    qApp->feedUpdateLock()->unlock();
    return;
  }

  auto editable_items = qlinq::from(items)
                          .where([](RootItem* it) {
                            return it->canBeEdited();
                          })
                          .distinct();

  if (editable_items.isEmpty()) {
    qApp->showGuiMessage(Notification::Event::GeneralEvent,
                         {tr("Cannot edit items"),
                          tr("Selected items cannot be edited. This is not supported (yet)."),
                          QSystemTrayIcon::MessageIcon::Critical});

    qApp->feedUpdateLock()->unlock();
    return;
  }

  // We also check if items are from single account, if not we end.
  auto distinct_accounts = editable_items
                             .select([](RootItem* it) {
                               return it->account();
                             })
                             .distinct();

  if (distinct_accounts.size() != 1) {
    qApp->showGuiMessage(Notification::Event::GeneralEvent,
                         {tr("Cannot edit items"),
                          tr("%1 does not support batch editing of items from multiple accounts.").arg(QSL(APP_NAME)),
                          QSystemTrayIcon::MessageIcon::Critical});

    qApp->feedUpdateLock()->unlock();
    return;
  }

  auto distinct_types = editable_items
                          .select([](RootItem* it) {
                            return it->kind();
                          })
                          .distinct();

  if (distinct_types.size() != 1) {
    qApp->showGuiMessage(Notification::Event::GeneralEvent,
                         {tr("Cannot edit items"),
                          tr("%1 does not support batch editing of items of varying types.").arg(QSL(APP_NAME)),
                          QSystemTrayIcon::MessageIcon::Critical});

    qApp->feedUpdateLock()->unlock();
    return;
  }

  if (editable_items.size() < items.size()) {
    // Some items are not editable.
    qApp->showGuiMessage(Notification::Event::GeneralEvent,
                         {tr("Cannot edit some items"),
                          tr("Some of selected items cannot be edited. Proceeding to edit the rest."),
                          QSystemTrayIcon::MessageIcon::Warning});
  }

  distinct_accounts.first()->editItems(editable_items.toList());

  // Changes are done, unlock the update master lock.
  qApp->feedUpdateLock()->unlock();
}

void FeedsView::editChildFeeds() {
  auto items = selectedFeeds(false);

  if (!items.isEmpty()) {
    auto root_items = qlinq::from(items).ofType<RootItem*>().toList();

    editItems(root_items);
  }
}

void FeedsView::editRecursiveFeeds() {
  auto items = selectedFeeds(true);

  if (!items.isEmpty()) {
    auto root_items = qlinq::from(items).ofType<RootItem*>().toList();

    editItems(root_items);
  }
}

void FeedsView::changeFilter(FeedsProxyModel::FeedListFilter filter) {
  m_proxyModel->setFeedListFilter(filter);

  QTimer::singleShot(1000, this, [this]() {
    if (!selectionModel()->selectedRows().isEmpty()) {
      scrollTo(selectionModel()->selectedRows().at(0), QAbstractItemView::ScrollHint::EnsureVisible);
    }
  });
}

void FeedsView::editSelectedItems() {
  editItems(selectedItems());
}

void FeedsView::deleteSelectedItem() {
  if (!qApp->feedUpdateLock()->tryLock()) {
    // Lock was not obtained because
    // it is used probably by feed updater or application
    // is quitting.
    qApp->showGuiMessage(Notification::Event::GeneralEvent,
                         {tr("Cannot delete item"),
                          tr("Selected item cannot be deleted because another critical operation is ongoing."),
                          QSystemTrayIcon::MessageIcon::Warning});

    // Thus, cannot delete and quit the method.
    return;
  }

  /*
  if (!currentIndex().isValid()) {
    qApp->feedUpdateLock()->unlock();
    return;
  }
  */

  QList<RootItem*> selected_items = selectedItems();
  auto deletable_items = qlinq::from(selected_items).where([](RootItem* it) {
    return it->canBeDeleted();
  });

  if (deletable_items.isEmpty()) {
    qApp->feedUpdateLock()->unlock();
    return;
  }

  if (deletable_items.size() < selected_items.size()) {
    qApp->showGuiMessage(Notification::Event::GeneralEvent,
                         GuiMessage(tr("Some items won't be deleted"),
                                    tr("Some selected items will not be deleted, because they cannot be deleted."),
                                    QSystemTrayIcon::MessageIcon::Warning));
  }

  // Ask user first.
  if (MsgBox::show({},
                   QMessageBox::Icon::Question,
                   tr("Deleting %n items", nullptr, int(deletable_items.size())),
                   tr("You are about to completely delete %n items.", nullptr, int(deletable_items.size())),
                   tr("Are you sure?"),
                   QString(),
                   QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No,
                   QMessageBox::StandardButton::Yes,
                   QSL("delete_selected_items")) == QMessageBox::StandardButton::No) {
    // User refused.
    qApp->feedUpdateLock()->unlock();
    return;
  }

  auto pointed_items = deletable_items
                         .select([](RootItem* it) {
                           return QPointer<RootItem>(it);
                         })
                         .toList();

  try {
    /*
    FormProgressWorker worker(qApp->mainFormWidget());

    worker.doSingleWork(
      tr("Deleting %n items", nullptr, pointed_items.size()),
      true,
      [&](QFutureWatcher<void>& rprt) {
        int progress = 0;
        emit rprt.progressRangeChanged(0, pointed_items.size() - 1);
        for (const QPointer<RootItem>& pnt : pointed_items) {
          if (pnt.isNull()) {
            continue;
          }

          pnt->deleteItem();
          emit rprt.progressValueChanged(++progress);
        }
      },
      [](int progress) {
        return tr("Deleted %n items...", nullptr, progress);
      });
    */

    for (const QPointer<RootItem>& pnt : pointed_items) {
      if (pnt.isNull()) {
        continue;
      }

      pnt->deleteItem();
    }

    m_sourceModel->reloadCountsOfWholeModel();
  }
  catch (const ApplicationException& ex) {
    qCriticalNN << LOGSEC_CORE << "Failed to delete item:" << NONQUOTE_W_SPACE_DOT(ex.message());
    qApp->showGuiMessage(Notification::Event::GeneralEvent,
                         GuiMessage(tr("Cannot delete item"),
                                    tr("Failed to delete selected item: %1.").arg(ex.message()),
                                    QSystemTrayIcon::MessageIcon::Critical),
                         GuiMessageDestination(true, true));
  }

  // Changes are done, unlock the update master lock.
  qApp->feedUpdateLock()->unlock();
}

void FeedsView::moveSelectedItemUp() {
  auto its = qlinq::from(selectedItems()).orderBy([](RootItem* it) {
    return it->sortOrder();
  });

  for (RootItem* it : its) {
    m_sourceModel->changeSortOrder(it, false, false, it->sortOrder() - 1);
  }

  m_proxyModel->invalidate();
}

void FeedsView::moveSelectedItemDown() {
  auto its = qlinq::from(selectedItems()).orderByDescending([](RootItem* it) {
    return it->sortOrder();
  });

  for (RootItem* it : its) {
    m_sourceModel->changeSortOrder(it, false, false, it->sortOrder() + 1);
  }

  m_proxyModel->invalidate();
}

void FeedsView::moveSelectedItemTop() {
  for (RootItem* it : selectedItems()) {
    m_sourceModel->changeSortOrder(it, true, false);
  }

  m_proxyModel->invalidate();
}

void FeedsView::moveSelectedItemBottom() {
  for (RootItem* it : selectedItems()) {
    m_sourceModel->changeSortOrder(it, false, true);
  }

  m_proxyModel->invalidate();
}

void FeedsView::rearrangeCategoriesOfSelectedItem() {
  for (RootItem* it : selectedItems()) {
    m_sourceModel->sortDirectDescendants(it, RootItem::Kind::Category);
  }

  m_proxyModel->invalidate();
}

void FeedsView::rearrangeFeedsOfSelectedItem() {
  for (RootItem* it : selectedItems()) {
    m_sourceModel->sortDirectDescendants(it, RootItem::Kind::Feed);
  }

  m_proxyModel->invalidate();
}

void FeedsView::markSelectedItemReadStatus(RootItem::ReadStatus read) {
  try {
    for (RootItem* it : selectedItems()) {
      m_sourceModel->markItemRead(it, read);
    }
  }
  catch (const ApplicationException& ex) {
    qCriticalNN << LOGSEC_CORE << "Cannot mark item read unread:" << NONQUOTE_W_SPACE_DOT(ex.message());
    qApp->showGuiMessage(Notification::Event::GeneralEvent,
                         GuiMessage(tr("Cannot mark item read unread"),
                                    tr("Failed to mark item read or unread: %1.").arg(ex.message()),
                                    QSystemTrayIcon::MessageIcon::Critical),
                         GuiMessageDestination(true, true));
  }
}

void FeedsView::markSelectedItemRead() {
  markSelectedItemReadStatus(RootItem::ReadStatus::Read);
}

void FeedsView::markSelectedItemUnread() {
  markSelectedItemReadStatus(RootItem::ReadStatus::Unread);
}

void FeedsView::markAllItemsReadStatus(RootItem::ReadStatus read) {
  try {
    m_sourceModel->markItemRead(m_sourceModel->rootItem(), read);
  }
  catch (const ApplicationException& ex) {
    qCriticalNN << LOGSEC_CORE << "Cannot mark item read unread:" << NONQUOTE_W_SPACE_DOT(ex.message());
    qApp->showGuiMessage(Notification::Event::GeneralEvent,
                         GuiMessage(tr("Cannot mark item read unread"),
                                    tr("Failed to mark item read or unread: %1.").arg(ex.message()),
                                    QSystemTrayIcon::MessageIcon::Critical),
                         GuiMessageDestination(true, true));
  }
}

void FeedsView::markAllItemsRead() {
  auto res = MsgBox::show({},
                          QMessageBox::Icon::Question,
                          tr("Mark everything as read"),
                          tr("Do you really want to mark everything as read?"),
                          {},
                          {},
                          QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No,
                          QMessageBox::StandardButton::Yes,
                          QSL("mark_everything_read"));

  if (res != QMessageBox::StandardButton::Yes) {
    return;
  }

  markAllItemsReadStatus(RootItem::ReadStatus::Read);
}

void FeedsView::selectNextItem() {
  QModelIndex index_next = moveCursor(QAbstractItemView::CursorAction::MoveDown, Qt::KeyboardModifier::NoModifier);

  if (index_next.isValid()) {
    setCurrentIndex(index_next);
    scrollTo(index_next, QAbstractItemView::ScrollHint::EnsureVisible);
  }

  setFocus();
}

void FeedsView::selectPreviousItem() {
  QModelIndex index_previous = moveCursor(QAbstractItemView::CursorAction::MoveUp, Qt::KeyboardModifier::NoModifier);

  if (index_previous.isValid()) {
    setCurrentIndex(index_previous);
    scrollTo(index_previous, QAbstractItemView::ScrollHint::EnsureVisible);
  }

  setFocus();
}

void FeedsView::selectNextUnreadItem() {
  QModelIndex next_unread_row;

  if (currentIndex().isValid()) {
    next_unread_row = nextPreviousUnreadItem(currentIndex());
  }
  else {
    next_unread_row = nextPreviousUnreadItem(m_proxyModel->index(0, MSG_MDL_READ_INDEX));
  }

  if (next_unread_row.isValid()) {
    setCurrentIndex(next_unread_row);
    scrollTo(next_unread_row, QAbstractItemView::ScrollHint::EnsureVisible);
    emit requestViewNextUnreadMessage();
  }
}

QModelIndex FeedsView::nextPreviousUnreadItem(const QModelIndex& default_row) {
  const bool started_from_zero = default_row.row() == 0 && !default_row.parent().isValid();
  QModelIndex next_index = nextUnreadItem(default_row);

  // There is no next message, check previous.
  if (!next_index.isValid() && !started_from_zero) {
    next_index = nextUnreadItem(m_proxyModel->index(0, 0));
  }

  return next_index;
}

QModelIndex FeedsView::nextUnreadItem(const QModelIndex& default_row) {
  QModelIndex nconst_default_row = m_proxyModel->index(default_row.row(), 0, default_row.parent());
  const QModelIndex starting_row = default_row;

  while (true) {
    bool has_unread =
      m_sourceModel->itemForIndex(m_proxyModel->mapToSource(nconst_default_row))->countOfUnreadMessages() > 0;

    if (has_unread) {
      if (m_proxyModel->hasChildren(nconst_default_row)) {
        // Current index has unread items, but is expandable, go to first child.
        expand(nconst_default_row);
        nconst_default_row = indexBelow(nconst_default_row);
        continue;
      }
      else {
        // We found unread feed, return it.
        return nconst_default_row;
      }
    }
    else {
      QModelIndex next_row = indexBelow(nconst_default_row);

      if (next_row == nconst_default_row || !next_row.isValid() || starting_row == next_row) {
        // We came to last row probably.
        break;
      }
      else {
        nconst_default_row = next_row;
      }
    }
  }

  return QModelIndex();
}

void FeedsView::switchVisibility() {
  setVisible(!isVisible());
}

void FeedsView::drawBranches(QPainter* painter, const QRect& rect, const QModelIndex& index) const {
  if (!rootIsDecorated()) {
    painter->save();
    painter->setOpacity(0.0);
  }

  QTreeView::drawBranches(painter, rect, index);

  if (!rootIsDecorated()) {
    painter->restore();
  }
}

void FeedsView::searchItems(SearchLineEdit::SearchMode mode,
                            Qt::CaseSensitivity sensitivity,
                            int custom_criteria,
                            const QString& phrase) {
  qDebugNN << LOGSEC_GUI << "Running search of feeds with pattern" << QUOTE_W_SPACE_DOT(phrase);

  switch (mode) {
    case SearchLineEdit::SearchMode::Wildcard:
      m_proxyModel->setFilterRegularExpression(QRegularExpression::wildcardToRegularExpression(phrase));
      break;

    case SearchLineEdit::SearchMode::RegularExpression:
      m_proxyModel->setFilterRegularExpression(QRegularExpression(phrase));
      break;

    case SearchLineEdit::SearchMode::FixedString:
    default:
      m_proxyModel->setFilterRegularExpression(QRegularExpression::escape(phrase));
      break;
  }

  m_proxyModel->setFilterCaseSensitivity(sensitivity);

  BaseToolBar::SearchFields where_search = BaseToolBar::SearchFields(custom_criteria);

  m_proxyModel->setFilterKeyColumn(where_search == BaseToolBar::SearchFields::SearchTitleOnly ? FDS_MODEL_TITLE_INDEX
                                                                                              : -1);

  QTimer::singleShot(200, this, [=]() {
    if (phrase.isEmpty()) {
      loadAllExpandStates();
    }
    else {
      m_dontSaveExpandState = true;
      expandAll();
      m_dontSaveExpandState = false;
    }

    if (!selectionModel()->selectedRows().isEmpty()) {
      scrollTo(selectionModel()->selectedRows().at(0), QAbstractItemView::ScrollHint::EnsureVisible);
    }
  });
}

void FeedsView::toggleFeedSortingMode(bool sort_alphabetically) {
  m_proxyModel->setSortAlphabetically(sort_alphabetically);
}

void FeedsView::onIndexExpanded(const QModelIndex& idx) {
  qDebugNN << LOGSEC_GUI << "Feed list item expanded - " << m_proxyModel->data(idx).toString();

  if (m_dontSaveExpandState) {
    qWarningNN << LOGSEC_GUI << "Don't saving expand state - " << m_proxyModel->data(idx).toString();
    return;
  }

  const RootItem* it = m_sourceModel->itemForIndex(m_proxyModel->mapToSource(idx));

  if (it != nullptr && (int(it->kind()) & int(RootItem::Kind::Category | RootItem::Kind::ServiceRoot |
                                              RootItem::Kind::Labels | RootItem::Kind::Probes)) > 0) {
    const QString setting_name = it->hashCode();

    qApp->settings()->setValue(GROUP(CategoriesExpandStates), setting_name, true);
  }
}

void FeedsView::onIndexCollapsed(const QModelIndex& idx) {
  qDebugNN << LOGSEC_GUI << "Feed list item collapsed - " << m_proxyModel->data(idx).toString();

  if (m_dontSaveExpandState) {
    qWarningNN << LOGSEC_GUI << "Don't saving collapse state - " << m_proxyModel->data(idx).toString();
    return;
  }

  RootItem* it = m_sourceModel->itemForIndex(m_proxyModel->mapToSource(idx));

  if (it != nullptr && (int(it->kind()) & int(RootItem::Kind::Category | RootItem::Kind::ServiceRoot |
                                              RootItem::Kind::Labels | RootItem::Kind::Probes)) > 0) {
    const QString setting_name = it->hashCode();

    qApp->settings()->setValue(GROUP(CategoriesExpandStates), setting_name, false);
  }
}

void FeedsView::reloadDelayedExpansions() {
  qDebugNN << LOGSEC_GUI << "Reloading delayed feed list expansions.";

  m_expansionDelayer.stop();
  m_dontSaveExpandState = true;

  auto expansions = m_delayedItemExpansions;

  for (const QModelIndex& idx : expansions) {
    auto idx_src = m_proxyModel->mapFromSource(idx);

    if (idx_src.isValid()) {
      setExpanded(idx_src, true);
    }
  }

  m_dontSaveExpandState = false;
  m_delayedItemExpansions.clear();
}

void FeedsView::loadAllExpandStates() {
  const Settings* settings = qApp->settings();
  QList<RootItem*> expandable_items;

  expandable_items.append(sourceModel()->rootItem()->getSubTree(RootItem::Kind::Category | RootItem::Kind::ServiceRoot |
                                                                RootItem::Kind::Labels | RootItem::Kind::Probes));

  // Iterate all categories and save their expand statuses.
  for (const RootItem* item : expandable_items) {
    const QString setting_name = item->hashCode();

    setExpanded(model()->mapFromSource(sourceModel()->indexForItem(item)),
                settings->value(GROUP(CategoriesExpandStates), setting_name, item->childCount() > 0).toBool());
  }

  sortByColumn(qApp->settings()->value(GROUP(GUI), SETTING(GUI::DefaultSortColumnFeeds)).toInt(),
               static_cast<Qt::SortOrder>(qApp->settings()
                                            ->value(GROUP(GUI), SETTING(GUI::DefaultSortOrderFeeds))
                                            .toInt()));
}

void FeedsView::reloadItemExpandState(const QModelIndex& source_idx) {
  //  Model requests to expand some items as they are visible and there is
  //  a filter active, so they maybe were not visible before.
  if (!source_idx.isValid()) {
    return;
  }

  m_delayedItemExpansions.insert(source_idx);
  m_expansionDelayer.start(100);
}

QByteArray FeedsView::saveHeaderState() const {
  QJsonObject obj;

  obj[QSL("header_count")] = header()->count();

  // Store column attributes.
  for (int i = 0; i < header()->count(); i++) {
    obj[QSL("header_%1_size").arg(i)] = header()->sectionSize(i);
    obj[QSL("header_%1_hidden").arg(i)] = header()->isSectionHidden(i);
  }

  return QJsonDocument(obj).toJson(QJsonDocument::JsonFormat::Compact);
}

void FeedsView::restoreHeaderState(const QByteArray& dta) {
  QJsonObject obj = QJsonDocument::fromJson(dta).object();
  int saved_header_count = obj[QSL("header_count")].toInt();

  if (saved_header_count < header()->count()) {
    qWarningNN << LOGSEC_GUI << "Detected invalid state for feed list.";
    return;
  }

  // Restore column attributes.
  for (int i = 0; i < saved_header_count && i < header()->count(); i++) {
    int ss = obj[QSL("header_%1_size").arg(i)].toInt();
    bool ish = obj[QSL("header_%1_hidden").arg(i)].toBool();

    header()->resizeSection(i, ss);
    header()->setSectionHidden(i, ish);
  }
}

void FeedsView::revealItem(RootItem* item) {
  auto idx = m_proxyModel->mapFromSource(m_sourceModel->indexForItem(item));

  if (idx.isValid()) {
    scrollTo(idx, QTreeView::ScrollHint::PositionAtCenter);
    // selectionModel()->setCurrentIndex(idx, QItemSelectionModel::SelectionFlag::NoUpdate);
    // setFocus();
    m_delegate->flashItem(idx, this);

    m_ignoreItemSelectionChange = true;
    setCurrentIndex(idx);
    selectionModel()->select(idx,
                             QItemSelectionModel::SelectionFlag::Rows |
                               QItemSelectionModel::SelectionFlag::ClearAndSelect);
    m_ignoreItemSelectionChange = false;
  }
  else {
    qApp->showGuiMessage(Notification::Event::GeneralEvent,
                         GuiMessage(tr("Feed filtered out"),
                                    tr("Your feed is probably filtered out and cannot be revealed."),
                                    QSystemTrayIcon::MessageIcon::Warning));
  }
}

void FeedsView::setupAppearance() {
  header()->setStretchLastSection(false);
  header()->setCascadingSectionResizes(false);
  header()->setSectionsMovable(false);
  header()->setDefaultSectionSize(MESSAGES_VIEW_DEFAULT_COL);
  header()->setMinimumSectionSize(MESSAGES_VIEW_MINIMUM_COL);

  setUniformRowHeights(true);
  setAnimated(true);
  setSortingEnabled(true);
  setItemsExpandable(true);
  setAutoExpandDelay(800);
  setExpandsOnDoubleClick(true);
  setEditTriggers(QAbstractItemView::EditTrigger::DoubleClicked | QAbstractItemView::EditTrigger::EditKeyPressed);
  setIndentation(FEEDS_VIEW_INDENTATION);
  setAcceptDrops(true);
  viewport()->setAcceptDrops(true);
  setDragEnabled(true);
  setDropIndicatorShown(true);
  setDragDropMode(QAbstractItemView::DragDropMode::InternalMove);
  setRootIsDecorated(false);
  setSelectionMode(QAbstractItemView::SelectionMode::ExtendedSelection);
  setItemDelegate(m_delegate);
}

void FeedsView::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) {
  QTreeView::selectionChanged(selected, deselected);

  RootItem* selected_item = selectedItem();
  m_proxyModel->setSelectedItem(selected_item);

  if (m_ignoreItemSelectionChange) {
    return;
  }

  emit itemSelected(selected_item);

  if (!selectedIndexes().isEmpty() &&
      qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::AutoExpandOnSelection)).toBool()) {
    expand(selectedIndexes().constFirst());
  }
}

void FeedsView::keyPressEvent(QKeyEvent* event) {
  BaseTreeView::keyPressEvent(event);

  if (event->key() == Qt::Key::Key_Delete) {
    deleteSelectedItem();
  }
}

void FeedsView::contextMenuEvent(QContextMenuEvent* event) {
  const QModelIndex clicked_index = indexAt(event->pos());

  if (clicked_index.isValid()) {
    const auto items = selectedItems();

    if (items.isEmpty()) {
      return;
    }

    auto items_linq = qlinq::from(items);
    auto accounts = items_linq
                      .select([](RootItem* it) {
                        return it->account();
                      })
                      .distinct();

    if (accounts.count() > 1) {
      qApp->showGuiMessage(Notification::Event::GeneralEvent,
                           GuiMessage(tr("Not supported"),
                                      tr("Context menus with items from more than one account are not supported."),
                                      QSystemTrayIcon::MessageIcon::Critical),
                           GuiMessageDestination(true, true));
      return;
    }

    auto account = accounts.first();
    QMenu* base_menu = baseContextMenu(items);
    auto service_specific_items = account->contextMenuFeedsList(items);

    if (!service_specific_items.isEmpty()) {
      base_menu->addSeparator();
      base_menu->addActions(service_specific_items);
    }

    base_menu->exec(event->globalPos());
  }
  else {
    TreeViewColumnsMenu menu(header());
    menu.exec(event->globalPos());
  }
}

QMenu* FeedsView::baseContextMenu(const QList<RootItem*>& selected_items) {
  QMenu* menu = new QMenu(tr("Menu for feed list"), this);
  auto items_linq = qlinq::from(selected_items);
  ServiceRoot* account = selected_items.first()->account();
  auto kinds = items_linq
                 .select([](RootItem* it) {
                   return it->kind();
                 })
                 .distinct();
  auto cat_add = account->supportsCategoryAdding();
  auto feed_add = account->supportsFeedAdding();
  bool alphabetic_sort = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::SortAlphabetically)).toBool();

  QMap<QString, QList<QAction*>> action_sections;

  if (kinds.all([](RootItem::Kind knd) {
        return knd == RootItem::Kind::ServiceRoot || knd == RootItem::Kind::Feed || knd == RootItem::Kind::Category;
      })) {
    // Accounts, categories, feeds can be fetched.
    action_sections[QSL("1")].append(qApp->mainForm()->m_ui->m_actionUpdateSelectedItems);

    if (cat_add) {
      action_sections[QSL("4")].append(qApp->mainForm()->m_ui->m_actionAddCategoryIntoSelectedItem);
    }

    if (feed_add) {
      action_sections[QSL("4")].append(qApp->mainForm()->m_ui->m_actionAddFeedIntoSelectedItem);
    }
  }

  if (items_linq.all([](RootItem* item) {
        return item->canBeEdited();
      })) {
    action_sections[QSL("2")].append(qApp->mainForm()->m_ui->m_actionEditSelectedItem);
  }

  if (items_linq.all([](RootItem* item) {
        return item->canBeDeleted();
      })) {
    action_sections[QSL("2")].append(qApp->mainForm()->m_ui->m_actionDeleteSelectedItem);
  }

  if (kinds.all([](RootItem::Kind knd) {
        return knd == RootItem::Kind::ServiceRoot || knd == RootItem::Kind::Category;
      })) {
    action_sections[QSL("3")].append({qApp->mainForm()->m_ui->m_actionEditChildFeeds,
                                      qApp->mainForm()->m_ui->m_actionEditChildFeedsRecursive});

    action_sections[QSL("5")].append({qApp->mainForm()->m_ui->m_actionRearrangeCategories,
                                      qApp->mainForm()->m_ui->m_actionRearrangeFeeds});
  }

  if (kinds.all([](RootItem::Kind knd) {
        return knd == RootItem::Kind::ServiceRoot || knd == RootItem::Kind::Feed || knd == RootItem::Kind::Category;
      })) {
    action_sections[QSL("2")].append(QList<QAction*>{qApp->mainForm()->m_ui->m_actionPurgeSelectedItems,
                                                     qApp->mainForm()->m_ui->m_actionCopyUrlSelectedFeed});
  }

  if (items_linq.all([](RootItem* item) {
        return item->childCount() > 0;
      })) {
    action_sections[QSL("6")].append(QList<QAction*>{
      qApp->mainForm()->m_ui->m_actionExpandCollapseItem,
      qApp->mainForm()->m_ui->m_actionExpandCollapseItemRecursively,
    });
  }

  action_sections[QSL("7")].append(QList<QAction*>{qApp->mainForm()->m_ui->m_actionMarkSelectedItemsAsRead,
                                                   qApp->mainForm()->m_ui->m_actionMarkSelectedItemsAsUnread});

  if (!alphabetic_sort && kinds.all([](RootItem::Kind knd) {
        return knd == RootItem::Kind::ServiceRoot || knd == RootItem::Kind::Feed || knd == RootItem::Kind::Category;
      })) {
    action_sections[QSL("5")].append({qApp->mainForm()->m_ui->m_actionFeedMoveUp,
                                      qApp->mainForm()->m_ui->m_actionFeedMoveDown,
                                      qApp->mainForm()->m_ui->m_actionFeedMoveTop,
                                      qApp->mainForm()->m_ui->m_actionFeedMoveBottom});
  }

  for (auto it = action_sections.cbegin(); it != action_sections.cend(); ++it) {
    menu->addActions(it.value());

    auto next = it;
    ++next;

    if (next != action_sections.cend()) {
      menu->addSeparator();
    }
  }

  return menu;
}

void FeedsView::mouseDoubleClickEvent(QMouseEvent* event) {
  QTreeView::mouseDoubleClickEvent(event);

  auto* it = selectedItem();

  if (it != nullptr && it->kind() == RootItem::Kind::Feed) {
    updateSelectedItems();
  }
}

void FeedsView::saveSortState(int column, Qt::SortOrder order) {
  qApp->settings()->setValue(GROUP(GUI), GUI::DefaultSortColumnFeeds, column);
  qApp->settings()->setValue(GROUP(GUI), GUI::DefaultSortOrderFeeds, order);
}

void FeedsView::validateItemAfterDragDrop(const QModelIndex& source_index) {
  const QModelIndex mapped = m_proxyModel->mapFromSource(source_index);

  if (mapped.isValid()) {
    expand(mapped);
    setCurrentIndex(mapped);
  }
}

void FeedsView::onItemExpandRequested(const QList<RootItem*>& items, bool exp) {
  for (const RootItem* item : items) {
    QModelIndex source_index = m_sourceModel->indexForItem(item);
    QModelIndex proxy_index = m_proxyModel->mapFromSource(source_index);

    setExpanded(proxy_index, exp);
  }
}

void FeedsView::drawRow(QPainter* painter, const QStyleOptionViewItem& options, const QModelIndex& index) const {
  auto opts = options;

  opts.decorationAlignment = Qt::AlignmentFlag::AlignLeft | Qt::AlignmentFlag::AlignVCenter;

  BaseTreeView::drawRow(painter, opts, index);
}

void FeedsView::adjustColumns() {
  qDebugNN << LOGSEC_GUI << "Feeds list header geometries changed.";

  if (header()->count() > 0 && !m_columnsAdjusted) {
    m_columnsAdjusted = true;

    // Setup column resize strategies.
    for (int i = 0; i < header()->count(); i++) {
      if (i == FDS_MODEL_TITLE_INDEX) {
        continue;
      }

      header()->setSectionResizeMode(i, QHeaderView::ResizeMode::Interactive);
    }

    header()->setSectionResizeMode(FDS_MODEL_TITLE_INDEX, QHeaderView::ResizeMode::Stretch);
    hideColumn(FDS_MODEL_ID_INDEX);
  }
}
