/*
    This file is part of Akregator.

    SPDX-FileCopyrightText: 2004 Stanislav Karchebny <Stanislav.Karchebny@kdemail.net>
    SPDX-FileCopyrightText: 2004 Sashmit Bhaduri <smt@vfemail.net>
    SPDX-FileCopyrightText: 2005 Frank Osterfeld <osterfeld@kde.org>

    SPDX-License-Identifier: GPL-2.0-or-later WITH LicenseRef-Qt-Commercial-exception-1.0
*/

#include "mainwidget.h"
#include "config-akregator.h"

#include "abstractselectioncontroller.h"
#include "actionmanagerimpl.h"
#include "akregator_part.h"
#include "akregatorconfig.h"
#include "articlejobs.h"
#include "articlelistview.h"
#include "articleviewerwidget.h"
#include "createfeedcommand.h"
#include "createfoldercommand.h"
#include "deletesubscriptioncommand.h"
#include "editsubscriptioncommand.h"
#include "expireitemscommand.h"
#include "importfeedlistcommand.h"
#include "utils.h"
#include <PimCommon/BroadcastStatus>

#include "feedlist.h"
#include "fetchqueue.h"
#include "folder.h"
#include "framemanager.h"
#include "job/downloadarticlejob.h"
#include "kernel.h"
#include "mainframe.h"
#include "openurlrequest.h"
#include "progressmanager.h"
#include "selectioncontroller.h"
#include "subscriptionlistmodel.h"
#include "subscriptionlistview.h"
#include "tabwidget.h"
#include "treenode.h"
#include "types.h"
#include "whatsnew/whatsnewtranslations.h"
#include "widgets/searchbar.h"
#include <WebEngineViewer/ZoomActionMenu>

#include <KAboutData>
#include <KLocalizedString>
#include <KMessageBox>
#include <KShell>

#include <KToggleAction>

#include <QApplication>
#include <QClipboard>
#include <QDesktopServices>
#include <QDomDocument>
#include <QSplitter>
#include <QTimer>
#include <QUrlQuery>
#include <QVBoxLayout>

#include "webengine/webengineframe.h"
#include <PimCommon/NetworkManager>
#include <TextAddonsWidgets/NeedUpdateVersionUtils>
#include <TextAddonsWidgets/NeedUpdateVersionWidget>

#if HAVE_WHATSNEWSNGSUPPORT
#include <TextAddonsWidgets/WhatsNewMessageNgWidget>
#include <TextAddonsWidgets/WhatsNewNgDialog>
#else
#include <TextAddonsWidgets/WhatsNewDialog>
#include <TextAddonsWidgets/WhatsNewMessageWidget>
#endif

#include <algorithm>
#include <chrono>
#include <memory>
#if HAVE_ACTIVITY_SUPPORT
#include "activities/activitiesmanager.h"
#include "akregator_plasma_activities_debug.h"
#endif
using namespace std::chrono_literals;
using namespace Qt::Literals::StringLiterals;
using namespace Akregator;

MainWidget::~MainWidget()
{
    // if m_shuttingDown is false, slotOnShutdown was not called. That
    // means that not the whole app is shutdown, only the part. So it
    // should be no risk to do the cleanups now
    if (!m_shuttingDown) {
        slotOnShutdown();
    }
}

MainWidget::MainWidget(Part *part, QWidget *parent, ActionManagerImpl *actionManager, const QString &name)
    : QWidget(parent)
    , m_feedList()
    , m_actionManager(actionManager)
    , m_feedListManagementInterface(new FeedListManagementImpl)
{
    setObjectName(name);

    FeedListManagementInterface::setInstance(m_feedListManagementInterface);

    m_actionManager->initMainWidget(this);
    m_part = part;
    m_shuttingDown = false;
    m_displayingAboutPage = false;
    setFocusPolicy(Qt::StrongFocus);

    connect(m_part, &Part::signalSettingsChanged, m_actionManager, &ActionManagerImpl::slotSettingsChanged);

    auto topLayout = new QVBoxLayout(this);
    topLayout->setContentsMargins({});
    topLayout->setSpacing(0);
    if (TextAddonsWidgets::NeedUpdateVersionUtils::checkVersion()) {
        const auto status =
            TextAddonsWidgets::NeedUpdateVersionUtils::obsoleteVersionStatus(QLatin1String(AKREGATOR_RELEASE_VERSION_DATE), QDate::currentDate());
        if (status != TextAddonsWidgets::NeedUpdateVersionUtils::ObsoleteVersion::NotObsoleteYet) {
            auto needUpdateVersionWidget = new TextAddonsWidgets::NeedUpdateVersionWidget(this);
            topLayout->addWidget(needUpdateVersionWidget);
            needUpdateVersionWidget->setObsoleteVersion(status);
        }
    }

    QString newFeaturesMD5;
#if HAVE_WHATSNEWSNGSUPPORT
    const KAboutData aboutData = KAboutData::fromAppStreamForApplication();
    mReleasesInfo = aboutData.releases();
    if (!mReleasesInfo.isEmpty()) {
        newFeaturesMD5 = mReleasesInfo.constFirst().untranslatedDescription();
    }
#else
    WhatsNewTranslations translations;
    newFeaturesMD5 = translations.newFeaturesMD5();
#endif
    if (!newFeaturesMD5.isEmpty()) {
        const QString previousNewFeaturesMD5 = Settings::self()->previousNewFeaturesMD5();
        if (!previousNewFeaturesMD5.isEmpty()) {
            const bool hasNewFeature = (previousNewFeaturesMD5 != newFeaturesMD5);
            if (hasNewFeature) {
#if HAVE_WHATSNEWSNGSUPPORT
                auto whatsNewMessageWidget = new TextAddonsWidgets::WhatsNewMessageNgWidget(i18n("Akregator"), this);
                whatsNewMessageWidget->setReleases(mReleasesInfo);
                whatsNewMessageWidget->setObjectName(u"whatsNewMessageWidget"_s);
                topLayout->addWidget(whatsNewMessageWidget);
                Settings::self()->setPreviousNewFeaturesMD5(newFeaturesMD5);
                Settings::self()->save();
                whatsNewMessageWidget->animatedShow();
#else
                auto whatsNewMessageWidget = new TextAddonsWidgets::WhatsNewMessageWidget(this, i18n("Akregator"));
                whatsNewMessageWidget->setWhatsNewInfos(translations.createWhatsNewInfo());
                whatsNewMessageWidget->setObjectName(QStringLiteral("whatsNewMessageWidget"));
                topLayout->addWidget(whatsNewMessageWidget);
                Settings::self()->setPreviousNewFeaturesMD5(newFeaturesMD5);
                Settings::self()->save();
                whatsNewMessageWidget->animatedShow();
#endif
            }
        } else {
            Settings::self()->setPreviousNewFeaturesMD5(newFeaturesMD5);
            Settings::self()->save();
        }
    }

    m_horizontalSplitter = new QSplitter(Qt::Horizontal, this);

    m_horizontalSplitter->setOpaqueResize(true);
    m_horizontalSplitter->setChildrenCollapsible(false);
    topLayout->addWidget(m_horizontalSplitter);

    connect(Kernel::self()->fetchQueue(), &FetchQueue::signalStarted, this, &MainWidget::slotFetchingStarted);
    connect(Kernel::self()->fetchQueue(), &FetchQueue::signalStopped, this, &MainWidget::slotFetchingStopped);

    m_feedListView = new SubscriptionListView(m_horizontalSplitter);
    m_feedListView->setObjectName(QLatin1StringView("feedtree"));
    m_actionManager->initSubscriptionListView(m_feedListView);

    connect(m_feedListView, &SubscriptionListView::userActionTakingPlace, this, &MainWidget::ensureArticleTabVisible);

    m_tabWidget = new TabWidget(m_horizontalSplitter);
    m_actionManager->initTabWidget(m_tabWidget);

    connect(m_part, &Part::signalSettingsChanged, this, &MainWidget::slotSettingsChanged);

    connect(m_tabWidget, &TabWidget::signalCurrentFrameChanged, this, &MainWidget::slotCurrentFrameChanged);

    connect(m_tabWidget, &TabWidget::signalRemoveFrameRequest, Kernel::self()->frameManager(), &FrameManager::slotRemoveFrame);

    connect(m_tabWidget,
            SIGNAL(signalOpenUrlRequest(Akregator::OpenUrlRequest &)),
            Kernel::self()->frameManager(),
            SLOT(slotOpenUrlRequest(Akregator::OpenUrlRequest &)));

    connect(Kernel::self()->frameManager(), &FrameManager::signalFrameAdded, m_tabWidget, &TabWidget::slotAddFrame);

    connect(Kernel::self()->frameManager(), &FrameManager::signalSelectFrame, m_tabWidget, &TabWidget::slotSelectFrame);

    connect(Kernel::self()->frameManager(), &FrameManager::signalFrameRemoved, m_tabWidget, &TabWidget::slotRemoveFrame);

    connect(Kernel::self()->frameManager(), &FrameManager::signalRequestNewFrame, this, &MainWidget::slotRequestNewFrame);

    connect(Kernel::self()->frameManager(), &FrameManager::signalFrameRemoved, this, &MainWidget::slotFramesChanged);
    connect(Kernel::self()->frameManager(), &FrameManager::signalCompleted, this, &MainWidget::slotFramesChanged);

    connect(PimCommon::NetworkManager::self(), &PimCommon::NetworkManager::networkStatusChanged, this, &MainWidget::slotNetworkStatusChanged);

    m_tabWidget->setWhatsThis(i18n("You can view multiple articles in several open tabs."));

    m_mainTab = new QWidget(this);
    m_mainTab->setObjectName(QLatin1StringView("Article Tab"));
    m_mainTab->setWhatsThis(i18n("Articles list."));

    auto mainTabLayout = new QVBoxLayout(m_mainTab);
    mainTabLayout->setContentsMargins({});

    m_searchBar = new SearchBar(m_mainTab);
    if (!Settings::showQuickFilter()) {
        m_searchBar->hide();
    }
    connect(m_searchBar, &SearchBar::forceLostFocus, this, &MainWidget::slotSetFocusToViewer);
    m_articleSplitter = new QSplitter(Qt::Vertical, m_mainTab);
    m_articleSplitter->setObjectName(QLatin1StringView("panner2"));
    m_articleSplitter->setChildrenCollapsible(false);

    m_articleWidget = new QWidget(m_articleSplitter);
    auto articleWidgetLayout = new QVBoxLayout;
    m_articleWidget->setLayout(articleWidgetLayout);
    articleWidgetLayout->setContentsMargins({});
    articleWidgetLayout->setSpacing(0);

    m_articleListView = new ArticleListView;
    articleWidgetLayout->addWidget(m_searchBar);
    articleWidgetLayout->addWidget(m_articleListView);
    connect(m_articleListView, &ArticleListView::userActionTakingPlace, this, &MainWidget::ensureArticleTabVisible);

    m_selectionController = new SelectionController(this);
    m_selectionController->setArticleLister(m_articleListView);
    m_selectionController->setFeedSelector(m_feedListView);

    connect(m_searchBar, &SearchBar::signalSearch, m_selectionController, &AbstractSelectionController::setFilters);

    connect(m_part, &Part::signalSettingsChanged, m_selectionController, &AbstractSelectionController::settingsChanged);

    auto expansionHandler = new FolderExpansionHandler(this);
    connect(m_feedListView, &QTreeView::expanded, expansionHandler, &FolderExpansionHandler::itemExpanded);
    connect(m_feedListView, &QTreeView::collapsed, expansionHandler, &FolderExpansionHandler::itemCollapsed);

    m_selectionController->setFolderExpansionHandler(expansionHandler);

    connect(m_selectionController, &AbstractSelectionController::currentSubscriptionChanged, this, &MainWidget::slotNodeSelected);

    connect(m_selectionController, &AbstractSelectionController::currentArticleChanged, this, &MainWidget::slotArticleSelected);

    connect(m_selectionController, &AbstractSelectionController::articleDoubleClicked, this, &MainWidget::slotOpenArticleInBrowser);

    m_actionManager->initArticleListView(m_articleListView);

    connect(m_articleListView, &ArticleListView::signalMouseButtonPressed, this, &MainWidget::slotMouseButtonPressed);

    m_articleViewer = new ArticleViewerWidget(m_actionManager->actionCollection(), m_articleSplitter);
    m_articleListView->setFocusProxy(m_articleViewer);
    setFocusProxy(m_articleViewer);

    connect(m_articleViewer, &ArticleViewerWidget::showStatusBarMessage, this, &MainWidget::slotShowStatusBarMessage);
    connect(m_articleViewer,
            SIGNAL(signalOpenUrlRequest(Akregator::OpenUrlRequest &)),
            Kernel::self()->frameManager(),
            SLOT(slotOpenUrlRequest(Akregator::OpenUrlRequest &)));
    connect(m_searchBar, &SearchBar::signalSearch, m_articleViewer, &ArticleViewerWidget::setFilters);
    mainTabLayout->addWidget(m_articleSplitter);

    m_mainFrame = new MainFrame(this, m_mainTab);
    m_mainFrame->slotSetTitle(i18n("Articles"));
    m_mainFrame->setArticleViewer(m_articleViewer);
    connect(m_articleViewer->articleViewerWidgetNg()->articleViewerNg(), &ArticleViewerWebEngine::articleAction, this, &MainWidget::slotArticleAction);
    connect(m_tabWidget, &TabWidget::signalCopyInFrame, m_mainFrame, &MainFrame::slotCopyInFrame);
    connect(m_tabWidget, &TabWidget::signalPrintInFrame, m_mainFrame, &MainFrame::slotPrintInFrame);
    connect(m_tabWidget, &TabWidget::signalZoomChangedInFrame, m_mainFrame, &MainFrame::slotZoomChangeInFrame);
    connect(m_tabWidget, &TabWidget::signalPrintPreviewInFrame, m_mainFrame, &MainFrame::slotPrintPreviewInFrame);
    connect(m_tabWidget, &TabWidget::signalFindTextInFrame, m_mainFrame, &MainFrame::slotFindTextInFrame);
    connect(m_tabWidget, &TabWidget::signalTextToSpeechInFrame, m_mainFrame, &MainFrame::slotTextToSpeechInFrame);
    connect(m_tabWidget, &TabWidget::signalMute, m_mainFrame, &MainFrame::slotMute);

    Kernel::self()->frameManager()->slotAddFrame(m_mainFrame);

    const QList<int> sp1sizes = Settings::splitter1Sizes();
    if (sp1sizes.count() >= m_horizontalSplitter->count()) {
        m_horizontalSplitter->setSizes(sp1sizes);
    }
    const QList<int> sp2sizes = Settings::splitter2Sizes();
    if (sp2sizes.count() >= m_articleSplitter->count()) {
        m_articleSplitter->setSizes(sp2sizes);
    }

    if (!Settings::self()->disableIntroduction()) {
        m_articleWidget->hide();
        m_articleViewer->displayAboutPage();
        m_mainFrame->slotSetTitle(i18n("About"));
        m_displayingAboutPage = true;
    }

    m_fetchTimer = new QTimer(this);
    connect(m_fetchTimer, &QTimer::timeout, this, &MainWidget::slotDoIntervalFetches);
    m_fetchTimer->start(1min);

    // delete expired articles once per hour
    m_expiryTimer = new QTimer(this);
    connect(m_expiryTimer, &QTimer::timeout, this, &MainWidget::slotDeleteExpiredArticles);
    m_expiryTimer->start(1h);

    m_markReadTimer = new QTimer(this);
    m_markReadTimer->setSingleShot(true);
    connect(m_markReadTimer, &QTimer::timeout, this, &MainWidget::slotSetCurrentArticleReadDelayed);

    setFeedList(QSharedPointer<FeedList>(new FeedList(Kernel::self()->storage())));

    switch (Settings::viewMode()) {
    case CombinedView:
        slotCombinedView();
        break;
    case WidescreenView:
        slotWidescreenView();
        break;
    default:
        slotNormalView();
    }

    if (!Settings::resetQuickFilterOnNodeChange()) {
        m_searchBar->slotSetStatus(Settings::statusFilter());
        m_searchBar->slotSetText(Settings::textFilter());
    }
#if HAVE_ACTIVITY_SUPPORT
    Kernel::self()->activitiesManager()->setEnabled(Akregator::Settings::self()->plasmaActivitySupport());
    connect(Kernel::self()->activitiesManager(), &ActivitiesManager::activitiesChanged, this, [this]() {
        m_selectionController->activitiesChanged();
        qCDebug(AKREGATOR_PLASMA_ACTIVITIES_LOG) << " activities changed";
    });
#endif
}

void MainWidget::slotSettingsChanged()
{
    m_tabWidget->slotSettingsChanged();
    m_articleViewer->updateAfterConfigChanged();
}

void MainWidget::slotSetFocusToViewer()
{
    m_articleViewer->setFocus();
}

void MainWidget::slotOnShutdown()
{
    disconnect(m_tabWidget, &TabWidget::signalCurrentFrameChanged, this, &MainWidget::slotCurrentFrameChanged);

    m_shuttingDown = true;

    // close all pageviewers in a controlled way
    // fixes bug 91660, at least when no part loading data
    while (m_tabWidget->count() > 1) { // remove frames until only the main frame remains
        m_tabWidget->setCurrentIndex(m_tabWidget->count() - 1); // select last page
        m_tabWidget->slotRemoveCurrentFrame();
    }

    Kernel::self()->fetchQueue()->slotAbort();
    setFeedList(QSharedPointer<FeedList>());

    delete m_feedListManagementInterface;
    delete m_feedListView; // call delete here, so that the header settings will get saved
    delete m_articleListView; // same for this one

    delete m_mainTab;
    delete m_mainFrame;
    m_mainFrame = nullptr;

    Settings::self()->save();
}

void MainWidget::saveSettings()
{
    const QList<int> spl1 = m_horizontalSplitter->sizes();
    if (std::count(spl1.begin(), spl1.end(), 0) == 0) {
        Settings::setSplitter1Sizes(spl1);
    }
    const QList<int> spl2 = m_articleSplitter->sizes();
    if (std::count(spl2.begin(), spl2.end(), 0) == 0) {
        Settings::setSplitter2Sizes(spl2);
    }
    Settings::setViewMode(m_viewMode);
    Settings::self()->save();
}

void MainWidget::connectFrame(Akregator::WebEngineFrame *frame)
{
    connect(m_tabWidget, &TabWidget::signalCopyInFrame, frame, &WebEngineFrame::slotCopyInFrame);
    connect(m_tabWidget, &TabWidget::signalPrintInFrame, frame, &WebEngineFrame::slotPrintInFrame);
    connect(m_tabWidget, &TabWidget::signalZoomChangedInFrame, frame, &WebEngineFrame::slotZoomChangeInFrame);
    connect(m_tabWidget, &TabWidget::signalPrintPreviewInFrame, frame, &WebEngineFrame::slotPrintPreviewInFrame);
    connect(m_tabWidget, &TabWidget::signalFindTextInFrame, frame, &WebEngineFrame::slotFindTextInFrame);
    connect(m_tabWidget, &TabWidget::signalTextToSpeechInFrame, frame, &WebEngineFrame::slotTextToSpeechInFrame);
    connect(m_tabWidget, &TabWidget::signalMute, frame, &WebEngineFrame::slotMute);

    connect(frame, &WebEngineFrame::showStatusBarMessage, this, &MainWidget::slotShowStatusBarMessage);
    connect(frame, &WebEngineFrame::signalIconChanged, m_tabWidget, &TabWidget::slotSetIcon);
    connect(frame, &WebEngineFrame::webPageMutedOrAudibleChanged, m_tabWidget, &TabWidget::slotWebPageMutedOrAudibleChanged);
}

void MainWidget::slotRequestNewFrame(int &frameId)
{
    auto frame = new WebEngineFrame(m_actionManager->actionCollection(), m_tabWidget);
    connectFrame(frame);

    Kernel::self()->frameManager()->slotAddFrame(frame);

    frameId = frame->id();
}

void MainWidget::sendArticle(bool attach)
{
    QByteArray text;
    QString title;

    Frame *frame = Kernel::self()->frameManager()->currentFrame();

    if (frame && frame->id() > 0) { // are we in some other tab than the articlelist?
        text = frame->url().toString().toLatin1();
        title = frame->title();
    } else { // nah, we're in articlelist..
        const Article article = m_selectionController->currentArticle();
        if (!article.isNull()) {
            text = article.link().toDisplayString().toLatin1();
            title = Akregator::Utils::convertHtmlTags(article.title());
        }
    }

    if (text.isEmpty()) {
        return;
    }
    sendArticle(text, title, attach);
}

void MainWidget::cleanUpDownloadFile()
{
    for (QPointer<Akregator::DownloadArticleJob> job : std::as_const(mListDownloadArticleJobs)) {
        if (job) {
            job->forceCleanupTemporaryFile();
        }
    }
}

void MainWidget::sendArticle(const QByteArray &text, const QString &title, bool attach)
{
    if (attach) {
        QPointer<Akregator::DownloadArticleJob> download = new Akregator::DownloadArticleJob(this);
        download->setArticleUrl(QUrl(QString::fromUtf8(text)));
        download->setText(QString::fromUtf8(text));
        download->setTitle(title);
        mListDownloadArticleJobs.append(download);
        download->start();
    } else {
        QUrlQuery query;
        query.addQueryItem(QStringLiteral("subject"), title);
        query.addQueryItem(QStringLiteral("body"), QString::fromUtf8(text));
        QUrl url;
        url.setScheme(QStringLiteral("mailto"));
        url.setQuery(query);
        QDesktopServices::openUrl(url);
    }
}

void MainWidget::importFeedList(const QDomDocument &doc)
{
    auto cmd = new ImportFeedListCommand;
    cmd->setParentWidget(this);
    cmd->setFeedListDocument(doc);
    cmd->setTargetList(m_feedList);
    cmd->start();
}

void MainWidget::setFeedList(const QSharedPointer<FeedList> &list)
{
    if (list == m_feedList) {
        return;
    }
    const QSharedPointer<FeedList> oldList = m_feedList;

    if (oldList) {
        oldList->disconnect(this);
    }

    m_feedList = list;
    if (m_feedList) {
        connect(m_feedList.data(), &FeedList::unreadCountChanged, this, &MainWidget::slotSetTotalUnread);
    }

    slotSetTotalUnread();

    m_feedListManagementInterface->setFeedList(m_feedList);
    Kernel::self()->setFeedList(m_feedList);
    ProgressManager::self()->setFeedList(m_feedList);
    m_selectionController->setFeedList(m_feedList);

    slotDeleteExpiredArticles();
}

void MainWidget::deleteExpiredArticles(const QSharedPointer<FeedList> &list)
{
    if (!list) {
        return;
    }
    auto cmd = new ExpireItemsCommand(this);
    cmd->setParentWidget(this);
    cmd->setFeedList(list);
    cmd->setFeeds(list->feedIds());
    cmd->start();
}

void MainWidget::slotDeleteExpiredArticles()
{
    deleteExpiredArticles(m_feedList);
}

QDomDocument MainWidget::feedListToOPML()
{
    QDomDocument dom;
    if (m_feedList) {
        dom = m_feedList->toOpml();
    }
    return dom;
}

void MainWidget::addFeedToGroup(const QString &url, const QString &groupName)
{
    // Locate the group.
    const QList<TreeNode *> namedGroups = m_feedList->findByTitle(groupName);
    Folder *group = nullptr;
    for (TreeNode *const candidate : namedGroups) {
        if (candidate->isGroup()) {
            group = static_cast<Folder *>(candidate);
            break;
        }
    }

    if (!group) {
        auto g = new Folder(groupName);
        m_feedList->allFeedsFolder()->appendChild(g);
        group = g;
    }

    // Invoke the Add Feed dialog with url filled in.
    addFeed(url, nullptr, group, true);
}

void MainWidget::slotNormalView()
{
    if (m_viewMode == NormalView) {
        return;
    }

    if (m_viewMode == CombinedView) {
        m_articleWidget->show();

        const Article article = m_selectionController->currentArticle();

        if (!article.isNull()) {
            m_articleViewer->showArticle(article);
        } else {
            m_articleViewer->slotShowSummary(m_selectionController->selectedSubscription());
        }
    }

    m_articleSplitter->setOrientation(Qt::Vertical);
    m_viewMode = NormalView;

    Settings::setViewMode(m_viewMode);
}

void MainWidget::slotWidescreenView()
{
    if (m_viewMode == WidescreenView) {
        return;
    }

    if (m_viewMode == CombinedView) {
        m_articleWidget->show();

        Article article = m_selectionController->currentArticle();

        if (!article.isNull()) {
            m_articleViewer->showArticle(article);
        } else {
            m_articleViewer->slotShowSummary(m_selectionController->selectedSubscription());
        }
    }

    m_articleSplitter->setOrientation(Qt::Horizontal);
    m_viewMode = WidescreenView;

    Settings::setViewMode(m_viewMode);
}

void MainWidget::slotCombinedView()
{
    if (m_viewMode == CombinedView) {
        return;
    }

    m_articleListView->slotClear();
    m_articleWidget->hide();
    m_viewMode = CombinedView;

    Settings::setViewMode(m_viewMode);
}

void MainWidget::slotMoveCurrentNodeUp()
{
    TreeNode *current = m_selectionController->selectedSubscription();
    if (!current) {
        return;
    }
    TreeNode *prev = current->prevSibling();
    Folder *parent = current->parent();

    if (!prev || !parent) {
        return;
    }

    parent->removeChild(prev);
    parent->insertChild(prev, current);
    m_feedListView->ensureNodeVisible(current);
}

void MainWidget::slotMoveCurrentNodeDown()
{
    TreeNode *current = m_selectionController->selectedSubscription();
    if (!current) {
        return;
    }
    TreeNode *next = current->nextSibling();
    Folder *parent = current->parent();

    if (!next || !parent) {
        return;
    }

    parent->removeChild(current);
    parent->insertChild(current, next);
    m_feedListView->ensureNodeVisible(current);
}

void MainWidget::slotMoveCurrentNodeLeft()
{
    TreeNode *current = m_selectionController->selectedSubscription();
    if (!current || !current->parent() || !current->parent()->parent()) {
        return;
    }

    Folder *parent = current->parent();
    Folder *grandparent = current->parent()->parent();

    parent->removeChild(current);
    grandparent->insertChild(current, parent);
    m_feedListView->ensureNodeVisible(current);
}

void MainWidget::slotMoveCurrentNodeRight()
{
    TreeNode *current = m_selectionController->selectedSubscription();
    if (!current || !current->parent()) {
        return;
    }
    TreeNode *prev = current->prevSibling();

    if (prev && prev->isGroup()) {
        auto fg = static_cast<Folder *>(prev);
        current->parent()->removeChild(current);
        fg->appendChild(current);
        m_feedListView->ensureNodeVisible(current);
    }
}

void MainWidget::slotSendLink()
{
    sendArticle();
}

void MainWidget::slotSendFile()
{
    sendArticle(true);
}

void MainWidget::slotNodeSelected(TreeNode *node)
{
    m_markReadTimer->stop();

    if (m_displayingAboutPage) {
        m_mainFrame->slotSetTitle(i18n("Articles"));
        if (m_viewMode != CombinedView) {
            m_articleWidget->show();
        }
        if (Settings::showQuickFilter()) {
            m_searchBar->show();
        }
        m_displayingAboutPage = false;
    }

    m_tabWidget->setCurrentWidget(m_mainFrame);
    if (Settings::resetQuickFilterOnNodeChange()) {
        m_searchBar->slotClearSearch();
    }

    if (m_viewMode == CombinedView) {
        m_articleViewer->showNode(node);
    } else {
        m_articleViewer->slotShowSummary(node);
    }

    if (node) {
        m_mainFrame->setWindowTitle(node->title());
    }

    m_actionManager->slotNodeSelected(node);
}

void MainWidget::slotFeedAdd()
{
    Folder *group = nullptr;
    if (!m_selectionController->selectedSubscription()) {
        group = m_feedList->allFeedsFolder();
    } else {
        if (m_selectionController->selectedSubscription()->isGroup()) {
            group = static_cast<Folder *>(m_selectionController->selectedSubscription());
        } else {
            group = m_selectionController->selectedSubscription()->parent();
        }
    }

    TreeNode *const lastChild = !group->children().isEmpty() ? group->children().constLast() : nullptr;

    addFeed(QString(), lastChild, group, false);
}

void MainWidget::addFeed(const QString &url, TreeNode *after, Folder *parent, bool autoExec)
{
    auto cmd(new CreateFeedCommand(this));
    cmd->setParentWidget(this);
    cmd->setPosition(parent, after);
    cmd->setRootFolder(m_feedList->allFeedsFolder());
    cmd->setAutoExecute(autoExec);
    cmd->setUrl(url);
    cmd->setSubscriptionListView(m_feedListView);
    cmd->start();
}

void MainWidget::slotFeedAddGroup()
{
    auto cmd = new CreateFolderCommand(this);
    cmd->setParentWidget(this);
    cmd->setSelectedSubscription(m_selectionController->selectedSubscription());
    cmd->setRootFolder(m_feedList->allFeedsFolder());
    cmd->setSubscriptionListView(m_feedListView);
    cmd->start();
}

void MainWidget::slotFeedRemove()
{
    TreeNode *selectedNode = m_selectionController->selectedSubscription();

    // don't delete root element! (safety valve)
    if (!selectedNode || selectedNode == m_feedList->allFeedsFolder()) {
        return;
    }

    auto cmd = new DeleteSubscriptionCommand(this);
    cmd->setParentWidget(this);
    cmd->setSubscription(m_feedList, selectedNode->id());
    cmd->start();
}

void MainWidget::slotFeedModify()
{
    TreeNode *const node = m_selectionController->selectedSubscription();
    if (!node) {
        return;
    }
    auto cmd = new EditSubscriptionCommand(this);
    cmd->setParentWidget(this);
    cmd->setSubscription(m_feedList, node->id());
    cmd->setSubscriptionListView(m_feedListView);
    cmd->start();

#if HAVE_ACTIVITY_SUPPORT
    if (Kernel::self()->activitiesManager()->enabled()) {
        connect(cmd, &EditSubscriptionCommand::finished, this, [this]() {
            qCDebug(AKREGATOR_PLASMA_ACTIVITIES_LOG) << " EditSubscriptionCommand finished. Potential activities changed";
            m_selectionController->activitiesChanged();
        });
    }
#endif
}

void MainWidget::slotNextUnreadArticle()
{
    ensureArticleTabVisible();
    if (m_viewMode == CombinedView) {
        m_feedListView->slotNextUnreadFeed();
        return;
    }
    TreeNode *sel = m_selectionController->selectedSubscription();
    if (sel && sel->unread() > 0) {
        m_articleListView->slotNextUnreadArticle();
    } else {
        m_feedListView->slotNextUnreadFeed();
    }
}

void MainWidget::slotPrevUnreadArticle()
{
    ensureArticleTabVisible();
    if (m_viewMode == CombinedView) {
        m_feedListView->slotPrevUnreadFeed();
        return;
    }
    TreeNode *sel = m_selectionController->selectedSubscription();
    if (sel && sel->unread() > 0) {
        m_articleListView->slotPreviousUnreadArticle();
    } else {
        m_feedListView->slotPrevUnreadFeed();
    }
}

bool MainWidget::confirmMarkFeedAsRead(bool isSingleFeed, bool isGroup)
{
    QString msg, caption;
    if (isSingleFeed && !isGroup) {
        msg = i18n("<qt>Are you sure you want to mark <b>all articles in the feed</b> as read?</qt>");
        caption = i18nc("@title:window", "Mark Feed as Read");
    } else {
        if (!isGroup) {
            msg = i18n("<qt>Are you sure you want to mark <b>all feeds</b> as read?</qt>");
        } else {
            msg = i18n("<qt>Are you sure you want to mark <b>all feeds in the folder</b> as read?</qt>");
        }
        caption = i18nc("@title:window", "Mark Feeds as Read");
    }
    if (KMessageBox::warningContinueCancel(this,
                                           msg,
                                           caption,
                                           KStandardGuiItem::cont(),
                                           KStandardGuiItem::cancel(),
                                           QStringLiteral("Disable Mark Feed As Read Confirmation"))
        != KMessageBox::Continue) {
        return false;
    }
    return true;
}

void MainWidget::slotMarkAllFeedsRead()
{
    if (!confirmMarkFeedAsRead(false, false)) {
        return;
    }
    KJob *job = m_feedList->createMarkAsReadJob();
    connect(job, &KJob::finished, m_selectionController, &AbstractSelectionController::forceFilterUpdate);
    job->start();
}

void MainWidget::slotMarkAllRead()
{
    TreeNode *current = m_selectionController->selectedSubscription();
    if (!current || !confirmMarkFeedAsRead(true, current->isGroup())) {
        return;
    }
    KJob *job = current->createMarkAsReadJob();
    connect(job, &KJob::finished, m_selectionController, &AbstractSelectionController::forceFilterUpdate);
    job->start();
}

void MainWidget::slotSetTotalUnread()
{
    Q_EMIT signalUnreadCountChanged(m_feedList ? m_feedList->unread() : 0);
}

void MainWidget::slotDoIntervalFetches()
{
    if (!m_feedList) {
        return;
    }
#if 0
    // the following solid check apparently doesn't work reliably and causes
    // interval fetching not working although the user is actually online (but solid reports he's not
    const Networking::Status status = Solid::Networking::status();
    if (status != Networking::Connected && status != Networking::Unknown) {
        return;
    }
#endif
    m_feedList->addToFetchQueue(Kernel::self()->fetchQueue(), true);
}

void MainWidget::slotFetchCurrentFeed()
{
    if (!m_selectionController->selectedSubscription()) {
        return;
    }
    if (isNetworkAvailable()) {
        m_selectionController->selectedSubscription()->slotAddToFetchQueue(Kernel::self()->fetchQueue());
    } else {
        m_mainFrame->slotSetStatusText(i18n("Networking is not available."));
    }
}

void MainWidget::slotFetchAllFeeds()
{
    if (m_feedList && isNetworkAvailable()) {
        m_feedList->addToFetchQueue(Kernel::self()->fetchQueue());
    } else if (m_feedList) {
        m_mainFrame->slotSetStatusText(i18n("Networking is not available."));
    }
}

void MainWidget::slotFetchingStarted()
{
    m_mainFrame->slotSetState(Frame::Started);
    m_actionManager->action(QStringLiteral("feed_stop"))->setEnabled(true);
    m_mainFrame->slotSetStatusText(i18n("Fetching Feeds…"));
}

void MainWidget::slotFetchingStopped()
{
    m_mainFrame->slotSetState(Frame::Completed);
    m_actionManager->action(QStringLiteral("feed_stop"))->setEnabled(false);
    m_mainFrame->slotSetStatusText(QString());
}

void MainWidget::slotArticleSelected(const Akregator::Article &article)
{
    if (m_viewMode == CombinedView) {
        return;
    }

    m_markReadTimer->stop();

    Q_ASSERT(article.isNull() || article.feed());

    QList<Article> articles = m_selectionController->selectedArticles();
    Q_EMIT signalArticlesSelected(articles);

    auto const maai = qobject_cast<KToggleAction *>(m_actionManager->action(QStringLiteral("article_set_status_important")));
    Q_ASSERT(maai);
    maai->setChecked(article.keep());

    m_articleViewer->showArticle(article);
    if (m_selectionController->selectedArticles().isEmpty()) {
        m_articleListView->setCurrentIndex(m_selectionController->currentArticleIndex());
    }

    if (article.isNull() || article.status() == Akregator::Read) {
        return;
    }

    if (!Settings::useMarkReadDelay()) {
        return;
    }

    const int delay = Settings::markReadDelay();

    if (delay > 0) {
        m_markReadTimer->start(delay * 1000);
    } else {
        auto job = new Akregator::ArticleModifyJob;
        const Akregator::ArticleId aid = {article.feed()->xmlUrl(), article.guid()};
        job->setStatus(aid, Akregator::Read);
        job->start();
    }
}

void MainWidget::slotMouseButtonPressed(int button, const QUrl &url)
{
    if (button != Qt::MiddleButton) {
        return;
    }

    if (!url.isValid()) {
        return;
    }

    OpenUrlRequest req(url);

    switch (Settings::mMBBehaviour()) {
    case Settings::EnumMMBBehaviour::OpenInExternalBrowser:
        req.setOptions(OpenUrlRequest::Options::ExternalBrowser);
        break;
    case Settings::EnumMMBBehaviour::OpenInBackground:
        req.setOptions(OpenUrlRequest::Options::NewTab);
        req.setOpenInBackground(true);
        break;
    default:
        req.setOptions(OpenUrlRequest::Options::NewTab);
        req.setOpenInBackground(false);
    }

    Kernel::self()->frameManager()->slotOpenUrlRequest(req);
}

void MainWidget::slotOpenHomepage()
{
    Feed *feed = dynamic_cast<Feed *>(m_selectionController->selectedSubscription());

    if (!feed) {
        return;
    }

    QUrl url(feed->htmlUrl());

    if (url.isValid()) {
        OpenUrlRequest req(url);
        req.setOptions(OpenUrlRequest::Options::ExternalBrowser);
        Kernel::self()->frameManager()->slotOpenUrlRequest(req);
    }
}

void MainWidget::slotOpenSelectedArticlesInBrowser()
{
    const QList<Article> articles = m_selectionController->selectedArticles();

    for (const Akregator::Article &article : articles) {
        slotOpenArticleInBrowser(article);
    }
}

void MainWidget::slotOpenArticleInBrowser(const Akregator::Article &article)
{
    if (!article.isNull() && article.link().isValid()) {
        OpenUrlRequest req(article.link());
        req.setOptions(OpenUrlRequest::Options::ExternalBrowser);
        Kernel::self()->frameManager()->slotOpenUrlRequest(req);
    }
}

void MainWidget::openSelectedArticles(bool openInBackground)
{
    const QList<Article> articles = m_selectionController->selectedArticles();

    for (const Akregator::Article &article : articles) {
        const QUrl url = article.link();
        if (!url.isValid()) {
            continue;
        }

        OpenUrlRequest req(url);
        req.setOptions(OpenUrlRequest::Options::NewTab);
        if (openInBackground) {
            req.setOpenInBackground(true);
            Kernel::self()->frameManager()->slotOpenUrlRequest(req, false /*don't use settings for open in background*/);
        } else {
            Kernel::self()->frameManager()->slotOpenUrlRequest(req);
        }
    }
}

void MainWidget::currentArticleInfo(QString &link, QString &title)
{
    const Article article = m_selectionController->currentArticle();

    if (article.isNull()) {
        return;
    }
    if (article.link().isValid()) {
        link = article.link().url();
        title = Utils::convertHtmlTags(article.title());
    }
}

void MainWidget::updateQuickSearchLineText()
{
    m_searchBar->updateQuickSearchLineText(m_actionManager->quickSearchLineText());
}

void MainWidget::slotCopyLinkAddress()
{
    const Article article = m_selectionController->currentArticle();

    if (article.isNull()) {
        return;
    }

    if (article.link().isValid()) {
        const QString link = article.link().url();
        QClipboard *cb = QApplication::clipboard();
        cb->setText(link, QClipboard::Clipboard);
        // don't set url to selection as it's a no-no according to a fd.o spec
        // cb->setText(link, QClipboard::Selection);
    }
}

void MainWidget::slotToggleShowQuickFilter()
{
    if (Settings::showQuickFilter()) {
        Settings::setShowQuickFilter(false);
        m_searchBar->slotClearSearch();
        m_searchBar->hide();
    } else {
        Settings::setShowQuickFilter(true);
        if (!m_displayingAboutPage) {
            m_searchBar->show();
        }
    }
}

void MainWidget::slotArticleDelete()
{
    if (m_viewMode == CombinedView) {
        return;
    }

    const QList<Article> articles = m_selectionController->selectedArticles();

    QString msg;
    switch (articles.count()) {
    case 0:
        return;
    case 1:
        msg = i18n("<qt>Are you sure you want to delete article <b>%1</b>?</qt>", articles.first().title());
        break;
    default:
        msg = i18np("<qt>Are you sure you want to delete the selected article?</qt>",
                    "<qt>Are you sure you want to delete the %1 selected articles?</qt>",
                    articles.count());
    }

    if (KMessageBox::warningContinueCancel(this,
                                           msg,
                                           i18nc("@title:window", "Delete Article"),
                                           KStandardGuiItem::del(),
                                           KStandardGuiItem::cancel(),
                                           QStringLiteral("Disable delete article confirmation"))
        != KMessageBox::Continue) {
        return;
    }

    TreeNode *const selected = m_selectionController->selectedSubscription();

    if (selected) {
        selected->setNotificationMode(false);
    }

    auto job = new Akregator::ArticleDeleteJob;
    for (const Akregator::Article &i : articles) {
        Feed *const feed = i.feed();
        if (!feed) {
            continue;
        }
        const Akregator::ArticleId aid = {feed->xmlUrl(), i.guid()};
        job->appendArticleId(aid);
    }

    job->start();

    if (selected) {
        selected->setNotificationMode(true);
    }
}

void MainWidget::slotFramesChanged()
{
    // We need to wait till the frame is fully loaded
    QMetaObject::invokeMethod(m_part, &Akregator::Part::slotAutoSave, Qt::QueuedConnection);
}

void MainWidget::slotArticleToggleKeepFlag(bool)
{
    const QList<Article> articles = m_selectionController->selectedArticles();

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

    bool allFlagsSet = true;
    for (const Akregator::Article &i : articles) {
        allFlagsSet = allFlagsSet && i.keep();
        if (!allFlagsSet) {
            break;
        }
    }

    auto job = new Akregator::ArticleModifyJob;
    for (const Akregator::Article &i : articles) {
        const Akregator::ArticleId aid = {i.feed()->xmlUrl(), i.guid()};
        job->setKeep(aid, !allFlagsSet);
    }
    job->start();
}

namespace
{
void setArticleStatus(const QString &feedUrl, const QString &articleId, int status)
{
    if (!feedUrl.isEmpty() && !articleId.isEmpty()) {
        auto job = new Akregator::ArticleModifyJob;
        const Akregator::ArticleId aid = {feedUrl, articleId};
        job->setStatus(aid, status);
        job->start();
    }
}

void setSelectedArticleStatus(const Akregator::AbstractSelectionController *controller, int status)
{
    const QList<Akregator::Article> articles = controller->selectedArticles();

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

    auto job = new Akregator::ArticleModifyJob;
    for (const Akregator::Article &i : articles) {
        const Akregator::ArticleId aid = {i.feed()->xmlUrl(), i.guid()};
        job->setStatus(aid, status);
    }
    job->start();
}
}

void MainWidget::slotSetSelectedArticleRead()
{
    ::setSelectedArticleStatus(m_selectionController, Akregator::Read);
}

void MainWidget::slotSetSelectedArticleUnread()
{
    ::setSelectedArticleStatus(m_selectionController, Akregator::Unread);
}

void MainWidget::slotSetSelectedArticleNew()
{
    ::setSelectedArticleStatus(m_selectionController, Akregator::New);
}

void MainWidget::slotSetCurrentArticleReadDelayed()
{
    const Article article = m_selectionController->currentArticle();

    if (article.isNull()) {
        return;
    }

    auto const job = new Akregator::ArticleModifyJob;
    const Akregator::ArticleId aid = {article.feed()->xmlUrl(), article.guid()};
    job->setStatus(aid, Akregator::Read);
    job->start();
}

void MainWidget::slotShowStatusBarMessage(const QString &msg)
{
    PimCommon::BroadcastStatus::instance()->setStatusMsg(msg);
}

void MainWidget::readProperties(const KConfigGroup &config)
{
    if (!Settings::resetQuickFilterOnNodeChange()) {
        // read filter settings
        m_searchBar->slotSetText(config.readEntry("searchLine"));
        m_searchBar->slotSetStatus(config.readEntry("searchCombo").toInt());
    }
    const QString currentTabName = config.readEntry("CurrentTab");
    // Reopen tabs
    const QStringList childList = config.readEntry(QStringLiteral("Children"), QStringList());
    int currentFrameId = -1;
    for (const QString &framePrefix : childList) {
        auto const frame = new WebEngineFrame(m_actionManager->actionCollection(), m_tabWidget);
        frame->loadConfig(config, framePrefix + u'_');

        connectFrame(frame);
        Kernel::self()->frameManager()->slotAddFrame(frame);
        if (currentTabName == framePrefix) {
            currentFrameId = frame->id();
        }
    }
    if (currentFrameId != -1) {
        m_tabWidget->slotSelectFrame(currentFrameId);
    }
}

void MainWidget::saveProperties(KConfigGroup &config)
{
    // save filter settings
    const QString searchStr(m_searchBar->text());
    if (searchStr.isEmpty()) {
        config.deleteEntry("searchLine");
    } else {
        config.writeEntry("searchLine", m_searchBar->text());
    }
    config.writeEntry("searchCombo", static_cast<int>(m_searchBar->status()));

    Kernel::self()->frameManager()->saveProperties(config);
}

void MainWidget::ensureArticleTabVisible()
{
    m_tabWidget->setCurrentWidget(m_mainFrame);
}

void MainWidget::slotReloadAllTabs()
{
    m_tabWidget->slotReloadAllTabs();
}

bool MainWidget::isNetworkAvailable() const
{
    return PimCommon::NetworkManager::self()->isOnline();
}

void MainWidget::slotNetworkStatusChanged(bool status)
{
    if (status) {
        m_mainFrame->slotSetStatusText(i18n("Networking is available now."));
        this->slotFetchAllFeeds();
    } else {
        m_mainFrame->slotSetStatusText(i18n("Networking is not available."));
    }
}

void MainWidget::slotOpenSelectedArticles()
{
    openSelectedArticles(false);
}

void MainWidget::slotOpenSelectedArticlesInBackground()
{
    openSelectedArticles(true);
}

void MainWidget::slotCurrentFrameChanged(int frameId)
{
    Kernel::self()->frameManager()->slotChangeFrame(frameId);
    m_actionManager->zoomActionMenu()->setZoomFactor(Kernel::self()->frameManager()->currentFrame()->zoomFactor() * 100);
}

void MainWidget::slotFocusQuickSearch()
{
    m_searchBar->setFocusSearchLine();
}

void MainWidget::slotWhatsNew()
{
#if HAVE_WHATSNEWSNGSUPPORT
    TextAddonsWidgets::WhatsNewNgDialog dlg(i18n("Akregator"), this);
    dlg.setReleases(mReleasesInfo);
    dlg.exec();
#else
    WhatsNewTranslations translations;
    QPointer<TextAddonsWidgets::WhatsNewDialog> dlg = new TextAddonsWidgets::WhatsNewDialog(translations.createWhatsNewInfo(), this, i18n("Akregator"));
    dlg->updateInformations();
    dlg->exec();
    delete dlg;
#endif
}

void MainWidget::slotArticleAction(Akregator::ArticleViewerWebEngine::ArticleAction type, const QString &articleId, const QString &feed)
{
    switch (type) {
    case ArticleViewerWebEngine::DeleteAction: {
        auto job = new Akregator::ArticleDeleteJob;
        const Akregator::ArticleId aid = {feed, articleId};
        job->appendArticleId(aid);
        job->start();
        break;
    }
    case ArticleViewerWebEngine::MarkAsRead:
        ::setArticleStatus(feed, articleId, Akregator::Read);
        break;
    case ArticleViewerWebEngine::MarkAsUnRead:
        ::setArticleStatus(feed, articleId, Akregator::Unread);
        break;

    case ArticleViewerWebEngine::MarkAsImportant: {
        auto job = new Akregator::ArticleModifyJob;
        const Akregator::Article article = m_feedList->findArticle(feed, articleId);
        const Akregator::ArticleId aid = {feed, articleId};
        job->setKeep(aid, !article.keep());
        job->start();
        break;
    }
    case ArticleViewerWebEngine::SendUrlArticle:
        [[fallthrough]];
    case ArticleViewerWebEngine::SendFileArticle: {
        const Article article = m_feedList->findArticle(feed, articleId);
        const QByteArray text = article.link().toDisplayString().toLatin1();
        const QString title = Akregator::Utils::convertHtmlTags(article.title());
        if (text.isEmpty()) {
            return;
        }
        sendArticle(text, title, (type == ArticleViewerWebEngine::SendFileArticle));
        break;
    }
    case ArticleViewerWebEngine::OpenInBackgroundTab: {
        const Akregator::Article article = m_feedList->findArticle(feed, articleId);
        const QUrl url = article.link();
        if (url.isValid()) {
            OpenUrlRequest req(url);
            req.setOptions(OpenUrlRequest::Options::NewTab);
            req.setOpenInBackground(true);
            Kernel::self()->frameManager()->slotOpenUrlRequest(req, false /*don't use settings for open in background*/);
        }
        break;
    }
    case ArticleViewerWebEngine::OpenInExternalBrowser: {
        const Akregator::Article article = m_feedList->findArticle(feed, articleId);
        slotOpenArticleInBrowser(article);
        break;
    }
    }
}

#include "moc_mainwidget.cpp"
