/*

  Copyright (c) 2009-2019 Montel Laurent <montel@kde.org>

  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 "foldertreeview.h"
#include "util/mailutil_p.h"
#include "kernel/mailkernel.h"

#include <CollectionStatistics>
#include <CollectionStatisticsDelegate>
#include <EntityTreeModel>

#include <KConfigGroup>
#include <KGuiItem>
#include <KLocalizedString>
#include <QMenu>
#include <KMessageBox>

#include <QHeaderView>
#include <QMouseEvent>

using namespace MailCommon;

FolderTreeView::FolderTreeView(QWidget *parent, bool showUnreadCount)
    : Akonadi::EntityTreeView(parent)
{
    init(showUnreadCount);
}

FolderTreeView::FolderTreeView(KXMLGUIClient *xmlGuiClient, QWidget *parent, bool showUnreadCount)
    : Akonadi::EntityTreeView(xmlGuiClient, parent)
{
    init(showUnreadCount);
}

FolderTreeView::~FolderTreeView()
{
}

void FolderTreeView::disableSaveConfig()
{
    mbDisableSaveConfig = true;
}

void FolderTreeView::setTooltipsPolicy(FolderTreeWidget::ToolTipDisplayPolicy policy)
{
    if (mToolTipDisplayPolicy == policy) {
        return;
    }

    mToolTipDisplayPolicy = policy;
    Q_EMIT changeTooltipsPolicy(mToolTipDisplayPolicy);
    writeConfig();
}

void FolderTreeView::disableContextMenuAndExtraColumn()
{
    mbDisableContextMenuAndExtraColumn = true;
    const int nbColumn = header()->count();
    for (int i = 1; i < nbColumn; ++i) {
        setColumnHidden(i, true);
    }
}

void FolderTreeView::init(bool showUnreadCount)
{
    setIconSize(QSize(22, 22));
    setUniformRowHeights(true);
    mSortingPolicy = FolderTreeWidget::SortByCurrentColumn;
    mToolTipDisplayPolicy = FolderTreeWidget::DisplayAlways;

    header()->setContextMenuPolicy(Qt::CustomContextMenu);
    connect(header(), &QWidget::customContextMenuRequested,
            this, &FolderTreeView::slotHeaderContextMenuRequested);

    mCollectionStatisticsDelegate = new Akonadi::CollectionStatisticsDelegate(this);
    mCollectionStatisticsDelegate->setProgressAnimationEnabled(true);
    setItemDelegate(mCollectionStatisticsDelegate);
    mCollectionStatisticsDelegate->setUnreadCountShown(
        showUnreadCount && !header()->isSectionHidden(1));
}

void FolderTreeView::showStatisticAnimation(bool anim)
{
    mCollectionStatisticsDelegate->setProgressAnimationEnabled(anim);
}

void FolderTreeView::writeConfig()
{
    if (mbDisableSaveConfig) {
        return;
    }

    KConfigGroup myGroup(KernelIf->config(), "MainFolderView");
    myGroup.writeEntry("IconSize", iconSize().width());
    myGroup.writeEntry("ToolTipDisplayPolicy", (int)mToolTipDisplayPolicy);
    myGroup.writeEntry("SortingPolicy", (int)mSortingPolicy);
}

void FolderTreeView::readConfig()
{
    KConfigGroup myGroup(KernelIf->config(), "MainFolderView");
    int iIconSize = myGroup.readEntry("IconSize", iconSize().width());
    if (iIconSize < 16 || iIconSize > 32) {
        iIconSize = 22;
    }
    setIconSize(QSize(iIconSize, iIconSize));
    mToolTipDisplayPolicy
        = static_cast<FolderTreeWidget::ToolTipDisplayPolicy>(
              myGroup.readEntry("ToolTipDisplayPolicy",
                                static_cast<int>(FolderTreeWidget::DisplayAlways)));

    Q_EMIT changeTooltipsPolicy(mToolTipDisplayPolicy);

    setSortingPolicy(
        (FolderTreeWidget::SortingPolicy)myGroup.readEntry(
            "SortingPolicy", (int)FolderTreeWidget::SortByCurrentColumn), false);
}

void FolderTreeView::slotHeaderContextMenuRequested(const QPoint &pnt)
{
    if (mbDisableContextMenuAndExtraColumn) {
        readConfig();
        return;
    }

    // the menu for the columns
    QMenu menu;
    QAction *act = nullptr;
    const int nbColumn = header()->count();
    if (nbColumn > 1) {
        menu.addSection(i18n("View Columns"));
        for (int i = 1; i < nbColumn; ++i) {
            act = menu.addAction(model()->headerData(i, Qt::Horizontal).toString());
            act->setCheckable(true);
            act->setChecked(!header()->isSectionHidden(i));
            act->setData(QVariant(i));
            connect(act, &QAction::triggered, this, &FolderTreeView::slotHeaderContextMenuChangeHeader);
        }
    }

    menu.addSection(i18n("Icon Size"));

    static const int icon_sizes[] = { 16, 22, 32};

    QActionGroup *grp = new QActionGroup(&menu);
    const int nbElement((int)(sizeof(icon_sizes) / sizeof(int)));
    for (int i = 0; i < nbElement; ++i) {
        act = menu.addAction(QStringLiteral("%1x%2").arg(icon_sizes[ i ]).arg(icon_sizes[ i ]));
        act->setCheckable(true);
        grp->addAction(act);
        if (iconSize().width() == icon_sizes[ i ]) {
            act->setChecked(true);
        }
        act->setData(QVariant(icon_sizes[ i ]));

        connect(act, &QAction::triggered, this, &FolderTreeView::slotHeaderContextMenuChangeIconSize);
    }
    menu.addSection(i18n("Display Tooltips"));

    grp = new QActionGroup(&menu);

    act = menu.addAction(i18nc("@action:inmenu Always display tooltips", "Always"));
    act->setCheckable(true);
    grp->addAction(act);
    act->setChecked(mToolTipDisplayPolicy == FolderTreeWidget::DisplayAlways);
    act->setData(QVariant((int)FolderTreeWidget::DisplayAlways));
    connect(act, &QAction::triggered, this, &FolderTreeView::slotHeaderContextMenuChangeToolTipDisplayPolicy);

    act = menu.addAction(i18nc("@action:inmenu Never display tooltips.", "Never"));
    act->setCheckable(true);
    grp->addAction(act);
    act->setChecked(mToolTipDisplayPolicy == FolderTreeWidget::DisplayNever);
    act->setData(QVariant((int)FolderTreeWidget::DisplayNever));
    connect(act, &QAction::triggered, this, &FolderTreeView::slotHeaderContextMenuChangeToolTipDisplayPolicy);

    menu.addSection(i18nc("@action:inmenu", "Sort Items"));

    grp = new QActionGroup(&menu);

    act = menu.addAction(i18nc("@action:inmenu", "Automatically, by Current Column"));
    act->setCheckable(true);
    grp->addAction(act);
    act->setChecked(mSortingPolicy == FolderTreeWidget::SortByCurrentColumn);
    act->setData(QVariant((int)FolderTreeWidget::SortByCurrentColumn));
    connect(act, &QAction::triggered, this, &FolderTreeView::slotHeaderContextMenuChangeSortingPolicy);

    act = menu.addAction(i18nc("@action:inmenu", "Manually, by Drag And Drop"));
    act->setCheckable(true);
    grp->addAction(act);
    act->setChecked(mSortingPolicy == FolderTreeWidget::SortByDragAndDropKey);
    act->setData(QVariant((int)FolderTreeWidget::SortByDragAndDropKey));
    connect(act, &QAction::triggered, this, &FolderTreeView::slotHeaderContextMenuChangeSortingPolicy);

    menu.exec(header()->mapToGlobal(pnt));
}

void FolderTreeView::slotHeaderContextMenuChangeSortingPolicy(bool)
{
    QAction *act = qobject_cast< QAction * >(sender());
    if (!act) {
        return;
    }

    QVariant data = act->data();

    bool ok;
    int policy = data.toInt(&ok);
    if (!ok) {
        return;
    }

    setSortingPolicy((FolderTreeWidget::SortingPolicy)policy, true);
}

void FolderTreeView::setSortingPolicy(FolderTreeWidget::SortingPolicy policy, bool writeInConfig)
{
    if (mSortingPolicy == policy) {
        return;
    }

    mSortingPolicy = policy;
    switch (mSortingPolicy) {
    case FolderTreeWidget::SortByCurrentColumn:
        header()->setSectionsClickable(true);
        header()->setSortIndicatorShown(true);
        setSortingEnabled(true);
        Q_EMIT manualSortingChanged(false);
        break;

    case FolderTreeWidget::SortByDragAndDropKey:
        header()->setSectionsClickable(false);
        header()->setSortIndicatorShown(false);

        setSortingEnabled(false);   // hack for qutie bug: this call shouldn't be here at all
        Q_EMIT manualSortingChanged(true);

        break;
    default:
        // should never happen
        break;
    }
    if (writeInConfig) {
        writeConfig();
    }
}

void FolderTreeView::slotHeaderContextMenuChangeToolTipDisplayPolicy(bool)
{
    QAction *act = qobject_cast< QAction * >(sender());
    if (!act) {
        return;
    }

    QVariant data = act->data();

    bool ok;
    const int id = data.toInt(&ok);
    if (!ok) {
        return;
    }
    Q_EMIT changeTooltipsPolicy((FolderTreeWidget::ToolTipDisplayPolicy)id);
}

void FolderTreeView::slotHeaderContextMenuChangeHeader(bool)
{
    QAction *act = qobject_cast< QAction * >(sender());
    if (!act) {
        return;
    }

    QVariant data = act->data();

    bool ok;
    const int id = data.toInt(&ok);
    if (!ok) {
        return;
    }

    if (id > header()->count()) {
        return;
    }

    if (id == 1) {
        mCollectionStatisticsDelegate->setUnreadCountShown(!act->isChecked());
    }

    setColumnHidden(id, !act->isChecked());
}

void FolderTreeView::slotHeaderContextMenuChangeIconSize(bool)
{
    QAction *act = qobject_cast< QAction * >(sender());
    if (!act) {
        return;
    }

    QVariant data = act->data();

    bool ok;
    const int size = data.toInt(&ok);
    if (!ok) {
        return;
    }

    const QSize newIconSize(QSize(size, size));
    if (newIconSize == iconSize()) {
        return;
    }
    setIconSize(newIconSize);

    writeConfig();
}

void FolderTreeView::setCurrentModelIndex(const QModelIndex &index)
{
    if (index.isValid()) {
        clearSelection();
        scrollTo(index);
        selectionModel()->setCurrentIndex(index, QItemSelectionModel::Rows);
    }
}

void FolderTreeView::selectModelIndex(const QModelIndex &index)
{
    if (index.isValid()) {
        scrollTo(index);
        selectionModel()->select(
            index,
            QItemSelectionModel::Rows | QItemSelectionModel::Select
            |QItemSelectionModel::Current | QItemSelectionModel::Clear);
    }
}

void FolderTreeView::slotSelectFocusFolder()
{
    const QModelIndex index = currentIndex();
    if (index.isValid()) {
        setCurrentIndex(index);
    }
}

void FolderTreeView::slotFocusNextFolder()
{
    const QModelIndex nextFolder = selectNextFolder(currentIndex());

    if (nextFolder.isValid()) {
        expand(nextFolder);
        setCurrentModelIndex(nextFolder);
    }
}

QModelIndex FolderTreeView::selectNextFolder(const QModelIndex &current)
{
    QModelIndex below;
    if (current.isValid()) {
        model()->fetchMore(current);
        if (model()->hasChildren(current)) {
            expand(current);
            below = indexBelow(current);
        } else if (current.row() < model()->rowCount(model()->parent(current)) - 1) {
            below = model()->index(current.row() + 1, current.column(), model()->parent(current));
        } else {
            below = indexBelow(current);
        }
    }
    return below;
}

void FolderTreeView::slotFocusPrevFolder()
{
    const QModelIndex current = currentIndex();
    if (current.isValid()) {
        QModelIndex above = indexAbove(current);
        setCurrentModelIndex(above);
    }
}

void FolderTreeView::slotFocusFirstFolder()
{
    const QModelIndex first = moveCursor(QAbstractItemView::MoveHome, nullptr);
    if (first.isValid()) {
        setCurrentModelIndex(first);
    }
}

void FolderTreeView::slotFocusLastFolder()
{
    const QModelIndex last = moveCursor(QAbstractItemView::MoveEnd, nullptr);
    if (last.isValid()) {
        setCurrentModelIndex(last);
    }
}

void FolderTreeView::selectNextUnreadFolder(bool confirm)
{
    // find next unread collection starting from current position
    if (!trySelectNextUnreadFolder(currentIndex(), ForwardSearch, confirm)) {
        // if there is none, jump to the last collection and try again
        trySelectNextUnreadFolder(model()->index(0, 0), ForwardSearch, confirm);
    }
}

// helper method to find last item in the model tree
static QModelIndex lastChildOf(QAbstractItemModel *model, const QModelIndex &current)
{
    if (model->rowCount(current) == 0) {
        return current;
    }

    return lastChildOf(model, model->index(model->rowCount(current) - 1, 0, current));
}

void FolderTreeView::selectPrevUnreadFolder(bool confirm)
{
    // find next unread collection starting from current position
    if (!trySelectNextUnreadFolder(currentIndex(), BackwardSearch, confirm)) {
        // if there is none, jump to top and try again
        const QModelIndex index = lastChildOf(model(), QModelIndex());
        trySelectNextUnreadFolder(index, BackwardSearch, confirm);
    }
}

bool FolderTreeView::trySelectNextUnreadFolder(const QModelIndex &current, SearchDirection direction, bool confirm)
{
    QModelIndex index = current;
    while (true) {
        index = nextUnreadCollection(index, direction);

        if (!index.isValid()) {
            return false;
        }

        const Akonadi::Collection collection
            = index.data(Akonadi::EntityTreeModel::CollectionRole).value<Akonadi::Collection>();
        if (collection == Kernel::self()->trashCollectionFolder()
            || collection == Kernel::self()->outboxCollectionFolder()) {
            continue;
        }

        if (ignoreUnreadFolder(collection, confirm)) {
            continue;
        }

        if (allowedToEnterFolder(collection, confirm)) {
            expand(index);
            setCurrentIndex(index);
            selectModelIndex(index);
            return true;
        } else {
            return false;
        }
    }

    return false;
}

bool FolderTreeView::ignoreUnreadFolder(const Akonadi::Collection &collection, bool confirm) const
{
    if (!confirm) {
        return false;
    }

    // Skip drafts, sent mail and templates as well, when reading mail with the
    // space bar - but not when changing into the next folder with unread mail
    // via ctrl+ or ctrl- so we do this only if (confirm == true), which means
    // we are doing readOn.

    return collection == Kernel::self()->draftsCollectionFolder()
           || collection == Kernel::self()->templatesCollectionFolder()
           || collection == Kernel::self()->sentCollectionFolder();
}

bool FolderTreeView::allowedToEnterFolder(const Akonadi::Collection &collection, bool confirm) const
{
    if (!confirm) {
        return true;
    }

    // warn user that going to next folder - but keep track of
    // whether he wishes to be notified again in "AskNextFolder"
    // parameter (kept in the config file for kmail)
    const int result
        = KMessageBox::questionYesNo(
              const_cast<FolderTreeView *>(this),
              i18n("<qt>Go to the next unread message in folder <b>%1</b>?</qt>", collection.name()),
              i18n("Go to Next Unread Message"),
              KGuiItem(i18n("Go To")),
              KGuiItem(i18n("Do Not Go To")),   // defaults
              QStringLiteral(":kmail_AskNextFolder"), nullptr);

    return result == KMessageBox::Yes;
}

bool FolderTreeView::isUnreadFolder(const QModelIndex &current, QModelIndex &index, FolderTreeView::Move move, bool confirm)
{
    if (current.isValid()) {
        if (move == FolderTreeView::Next) {
            index = selectNextFolder(current);
        } else if (move == FolderTreeView::Previous) {
            index = indexAbove(current);
        }

        if (index.isValid()) {
            const Akonadi::Collection collection
                = index.model()->data(
                      current, Akonadi::EntityTreeModel::CollectionRole).value<Akonadi::Collection>();

            if (collection.isValid()) {
                if (collection.statistics().unreadCount() > 0) {
                    if (!confirm) {
                        selectModelIndex(current);
                        return true;
                    } else {
                        // Skip drafts, sent mail and templates as well, when reading mail with the
                        // space bar - but not when changing into the next folder with unread mail
                        // via ctrl+ or ctrl- so we do this only if (confirm == true), which means
                        // we are doing readOn.

                        if (collection == Kernel::self()->draftsCollectionFolder()
                            || collection == Kernel::self()->templatesCollectionFolder()
                            || collection == Kernel::self()->sentCollectionFolder()) {
                            return false;
                        }

                        // warn user that going to next folder - but keep track of
                        // whether he wishes to be notified again in "AskNextFolder"
                        // parameter (kept in the config file for kmail)
                        if (KMessageBox::questionYesNo(
                                this,
                                i18n("<qt>Go to the next unread message in folder <b>%1</b>?</qt>",
                                     collection.name()),
                                i18n("Go to Next Unread Message"),
                                KGuiItem(i18n("Go To")),
                                KGuiItem(i18n("Do Not Go To")),         // defaults
                                QStringLiteral(":kmail_AskNextFolder"),
                                nullptr) == KMessageBox::No) {
                            return true; // assume selected (do not continue looping)
                        }

                        selectModelIndex(current);
                        return true;
                    }
                }
            }
        }
    }
    return false;
}

Akonadi::Collection FolderTreeView::currentFolder() const
{
    const QModelIndex current = currentIndex();
    if (current.isValid()) {
        const Akonadi::Collection collection
            = current.model()->data(
                  current,
                  Akonadi::EntityTreeModel::CollectionRole).value<Akonadi::Collection>();
        return collection;
    }
    return Akonadi::Collection();
}

void FolderTreeView::mousePressEvent(QMouseEvent *e)
{
    const bool buttonPressedIsMiddle = (e->button() == Qt::MidButton);
    Q_EMIT newTabRequested(buttonPressedIsMiddle);
    EntityTreeView::mousePressEvent(e);
}

void FolderTreeView::restoreHeaderState(const QByteArray &data)
{
    if (data.isEmpty()) {
        const int nbColumn = header()->count();
        for (int i = 1; i < nbColumn; ++i) {
            setColumnHidden(i, true);
        }
    } else {
        header()->restoreState(data);
    }
    mCollectionStatisticsDelegate->setUnreadCountShown(header()->isSectionHidden(1));
}

void FolderTreeView::updatePalette()
{
    mCollectionStatisticsDelegate->updatePalette();
}

void FolderTreeView::keyboardSearch(const QString &)
{
    // Disable keyboardSearch: it interfers with filtering in the
    // FolderSelectionDialog. We don't want it in KMail main window
    // either because KMail has one-letter keyboard shortcuts.
}

QModelIndex FolderTreeView::indexBelow(const QModelIndex &current) const
{
    // if we have children, return first child
    if (model()->rowCount(current) > 0) {
        return model()->index(0, 0, current);
    }

    // if we have siblings, return next sibling
    const QModelIndex parent = model()->parent(current);
    const QModelIndex sibling = model()->index(current.row() + 1, 0, parent);

    if (sibling.isValid()) {   // found valid sibling
        return sibling;
    }

    if (!parent.isValid()) {   // our parent is the tree root and we have no siblings
        return QModelIndex(); // we reached the bottom of the tree
    }

    // We are the last child, the next index to check is our uncle, parent's first sibling
    const QModelIndex parentsSibling = parent.sibling(parent.row() + 1, 0);
    if (parentsSibling.isValid()) {
        return parentsSibling;
    }

    // iterate over our parents back to root until we find a parent with a valid sibling
    QModelIndex currentParent = parent;
    QModelIndex grandParent = model()->parent(currentParent);
    while (currentParent.isValid()) {
        // check if the parent has children except from us
        if (model()->rowCount(grandParent) > currentParent.row() + 1) {
            const auto index = indexBelow(model()->index(currentParent.row() + 1, 0, grandParent));
            if (index.isValid()) {
                return index;
            }
        }

        currentParent = grandParent;
        grandParent = model()->parent(currentParent);
    }

    return QModelIndex(); // nothing found -> end of tree
}

QModelIndex FolderTreeView::lastChild(const QModelIndex &current) const
{
    if (model()->rowCount(current) == 0) {
        return current;
    }

    return lastChild(model()->index(model()->rowCount(current) - 1, 0, current));
}

QModelIndex FolderTreeView::indexAbove(const QModelIndex &current) const
{
    const QModelIndex parent = model()->parent(current);

    if (current.row() == 0) {
        // we have no previous siblings -> our parent is the next item above us
        return parent;
    }

    // find previous sibling
    const QModelIndex previousSibling = model()->index(current.row() - 1, 0, parent);

    // the item above us is the last child (or grandchild, or grandgrandchild... etc)
    // of our previous sibling
    return lastChild(previousSibling);
}

QModelIndex FolderTreeView::nextUnreadCollection(const QModelIndex &current, SearchDirection direction) const
{
    QModelIndex index = current;
    while (true) {
        if (direction == ForwardSearch) {
            index = indexBelow(index);
        } else if (direction == BackwardSearch) {
            index = indexAbove(index);
        }

        if (!index.isValid()) {   // reach end or top of the model
            return QModelIndex();
        }

        // check if the index is a collection
        const auto collection = index.data(Akonadi::EntityTreeModel::CollectionRole).value<Akonadi::Collection>();

        if (collection.isValid()) {
            // check if it is unread
            if (collection.statistics().unreadCount() > 0) {
                if (!MailCommon::Util::ignoreNewMailInFolder(collection)) {
                    return index; // we found the next unread collection
                }
            }
        }
    }

    return QModelIndex(); // no unread collection found
}
