/*
    SPDX-FileCopyrightText: 2006 Thomas Baumgart <ipwizard@users.sourceforge.net>
    SPDX-FileCopyrightText: 2017 Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
    SPDX-License-Identifier: GPL-2.0-or-later
*/

#ifndef KGLOBALLEDGERVIEW_P_H
#define KGLOBALLEDGERVIEW_P_H

#include "kgloballedgerview.h"

// ----------------------------------------------------------------------------
// QT Includes

#include <QFrame>
#include <QHBoxLayout>
#include <QList>
#include <QLabel>
#include <QEvent>
#include <QVBoxLayout>
#include <QHeaderView>
#include <QToolTip>
#include <QMenu>
#include <QWidgetAction>

// ----------------------------------------------------------------------------
// KDE Includes

#include <KLocalizedString>
#include <KMessageBox>
#include <KToolBar>
#include <KPassivePopup>

// ----------------------------------------------------------------------------
// Project Includes

#include "accountsmodel.h"
#include "fancydategroupmarkers.h"
#include "icons.h"
#include "kbalancewarning.h"
#include "kendingbalancedlg.h"
#include "kfindtransactiondlg.h"
#include "kmergetransactionsdlg.h"
#include "kmymoneyaccountcombo.h"
#include "kmymoneyaccountselector.h"
#include "kmymoneysettings.h"
#include "kmymoneyutils.h"
#include "kmymoneyviewbase_p.h"
#include "menuenums.h"
#include "modelenums.h"
#include "models.h"
#include "mymoneyaccount.h"
#include "mymoneyenums.h"
#include "mymoneyexception.h"
#include "mymoneyfile.h"
#include "mymoneymoney.h"
#include "mymoneypayee.h"
#include "mymoneyprice.h"
#include "mymoneyschedule.h"
#include "mymoneysecurity.h"
#include "mymoneysplit.h"
#include "mymoneytracer.h"
#include "mymoneytransaction.h"
#include "mymoneytransactionfilter.h"
#include "register.h"
#include "registersearchline.h"
#include "scheduledtransaction.h"
#include "selectedtransactions.h"
#include "tabbar.h"
#include "transaction.h"
#include "transactioneditor.h"
#include "transactionform.h"
#include "transactionmatcher.h"
#include "widgetenums.h"

#include <config-kmymoney.h>
#ifdef KMM_DEBUG
#include "mymoneyutils.h"
#endif

using namespace eMenu;
using namespace eMyMoney;

/**
  * helper class implementing an event filter to detect mouse button press
  * events on widgets outside a given set of widgets. This is used internally
  * to detect when to leave the edit mode.
  */
class MousePressFilter : public QObject
{
    Q_OBJECT
public:
    explicit MousePressFilter(QWidget* parent = nullptr) :
        QObject(parent),
        m_lastMousePressEvent(0),
        m_filterActive(true)
    {
    }

    /**
      * Add widget @p w to the list of possible parent objects. See eventFilter() how
      * they will be used.
      */
    void addWidget(QWidget* w)
    {
        m_parents.append(w);
    }

public Q_SLOTS:
    /**
      * This slot allows to activate/deactivate the filter. By default the
      * filter is active.
      *
      * @param state Allows to activate (@a true) or deactivate (@a false) the filter
      */
    void setFilterActive(bool state = true)
    {
        m_filterActive = state;
    }

    /**
      * This slot allows to activate/deactivate the filter. By default the
      * filter is active.
      *
      * @param state Allows to deactivate (@a true) or activate (@a false) the filter
      */
    void setFilterDeactive(bool state = false) {
        setFilterActive(!state);
    }

protected:
    /**
      * This method checks if the widget @p child is a child of
      * the widget @p parent and returns either @a true or @a false.
      *
      * @param child pointer to child widget
      * @param parent pointer to parent widget
      * @retval true @p child points to widget which has @p parent as parent or grand-parent
      * @retval false @p child points to a widget which is not related to @p parent
      */
    bool isChildOf(QWidget* child, QWidget* parent)
    {
        // QDialogs cannot be detected directly, but it can be assumed,
        // that events on a widget that do not have a parent widget within
        // our application are dialogs.
        if (!child->parentWidget())
            return true;

        while (child) {
            // if we are a child of the given parent, we have a match
            if (child == parent)
                return true;
            // if we are at the application level, we don't have a match
            if (child->inherits("KMyMoneyApp"))
                return false;
            // If one of the ancestors is a KPassivePopup or a KDialog or a popup widget then
            // it's as if it is a child of our own because these widgets could
            // appear during transaction entry (message boxes, completer widgets)
            if (dynamic_cast<KPassivePopup*>(child) ||
                    ((child->windowFlags() & Qt::Popup) && /*child != kmymoney*/
                     !child->parentWidget())) // has no parent, then it must be top-level window
                return true;
            child = child->parentWidget();
        }
        return false;
    }

    /**
      * Reimplemented from base class. Sends out the mousePressedOnExternalWidget() signal
      * if object @p o points to an object which is not a child widget of any added previously
      * using the addWidget() method. The signal is sent out only once for each event @p e.
      *
      * @param o pointer to QObject
      * @param e pointer to QEvent
      * @return always returns @a false
      */
    bool eventFilter(QObject* o, QEvent* e) final override
    {
        if (m_filterActive) {
            if (e->type() == QEvent::MouseButtonPress && !m_lastMousePressEvent) {
                QWidget* w = qobject_cast<QWidget*>(o);
                if (!w) {
                    return QObject::eventFilter(o, e);
                }
                QList<QWidget*>::const_iterator it_w;
                for (it_w = m_parents.constBegin(); it_w != m_parents.constEnd(); ++it_w) {
                    if (isChildOf(w, (*it_w))) {
                        m_lastMousePressEvent = e;
                        break;
                    }
                }
                if (it_w == m_parents.constEnd()) {
                    m_lastMousePressEvent = e;
                    bool rc = false;
                    emit mousePressedOnExternalWidget(rc);
                }
            }

            if (e->type() != QEvent::MouseButtonPress) {
                m_lastMousePressEvent = 0;
            }
        }
        return false;
    }

Q_SIGNALS:
    void mousePressedOnExternalWidget(bool&);

private:
    QList<QWidget*>      m_parents;
    QEvent*              m_lastMousePressEvent;
    bool                 m_filterActive;
};

class KGlobalLedgerViewPrivate : public KMyMoneyViewBasePrivate
{
    Q_DECLARE_PUBLIC(KGlobalLedgerView)

public:
    explicit KGlobalLedgerViewPrivate(KGlobalLedgerView *qq) :
        q_ptr(qq),
        m_mousePressFilter(0),
        m_registerSearchLine(0),
        m_precision(2),
        m_recursion(false),
        m_showDetails(false),
        m_action(eWidgets::eRegister::Action::None),
        m_filterProxyModel(0),
        m_accountComboBox(0),
        m_balanceIsApproximated(false),
        m_toolbarFrame(nullptr),
        m_registerFrame(nullptr),
        m_buttonFrame(nullptr),
        m_formFrame(nullptr),
        m_summaryFrame(nullptr),
        m_register(nullptr),
        m_buttonbar(nullptr),
        m_leftSummaryLabel(nullptr),
        m_centerSummaryLabel(nullptr),
        m_rightSummaryLabel(nullptr),
        m_form(nullptr),
        m_needLoad(true),
        m_newAccountLoaded(true),
        m_inEditMode(false),
        m_transactionEditor(nullptr),
        m_balanceWarning(nullptr),
        m_moveToAccountSelector(nullptr),
        m_endingBalanceDlg(nullptr),
        m_searchDlg(nullptr)
    {
    }

    ~KGlobalLedgerViewPrivate()
    {
        delete m_moveToAccountSelector;
        delete m_endingBalanceDlg;
        delete m_searchDlg;
    }

    void init()
    {
        Q_Q(KGlobalLedgerView);
        m_needLoad = false;
        auto vbox = new QVBoxLayout(q);
        q->setLayout(vbox);
        vbox->setSpacing(6);
        vbox->setMargin(0);

        m_mousePressFilter = new MousePressFilter((QWidget*)q);
        m_action = eWidgets::eRegister::Action::None;

        // the proxy filter model
        m_filterProxyModel = new AccountNamesFilterProxyModel(q);
        m_filterProxyModel->addAccountGroup(QVector<eMyMoney::Account::Type>{eMyMoney::Account::Type::Asset,
                                                                             eMyMoney::Account::Type::Liability,
                                                                             eMyMoney::Account::Type::Equity,
                                                                             eMyMoney::Account::Type::Income,
                                                                             eMyMoney::Account::Type::Expense});
        auto const model = Models::instance()->accountsModel();
        m_filterProxyModel->setSourceColumns(model->getColumns());
        m_filterProxyModel->setSourceModel(model);
        m_filterProxyModel->sort((int)eAccountsModel::Column::Account);

        // create the toolbar frame at the top of the view
        m_toolbarFrame = new QFrame();
        QHBoxLayout* toolbarLayout = new QHBoxLayout(m_toolbarFrame);
        toolbarLayout->setContentsMargins(0, 0, 0, 0);
        toolbarLayout->setSpacing(6);

        // the account selector widget
        m_accountComboBox = new KMyMoneyAccountCombo();
        m_accountComboBox->setModel(m_filterProxyModel);
        toolbarLayout->addWidget(m_accountComboBox);

        vbox->addWidget(m_toolbarFrame);
        toolbarLayout->setStretchFactor(m_accountComboBox, 60);
        // create the register frame
        m_registerFrame = new QFrame();
        QVBoxLayout* registerFrameLayout = new QVBoxLayout(m_registerFrame);
        registerFrameLayout->setContentsMargins(0, 0, 0, 0);
        registerFrameLayout->setSpacing(0);
        vbox->addWidget(m_registerFrame);
        vbox->setStretchFactor(m_registerFrame, 2);
        m_register = new KMyMoneyRegister::Register(m_registerFrame);
        m_register->setUsedWithEditor(true);
        registerFrameLayout->addWidget(m_register);
        m_register->installEventFilter(q);
        q->connect(m_register, &KMyMoneyRegister::Register::openContextMenu, q, &KGlobalLedgerView::slotTransactionsContextMenuRequested);
        q->connect(m_register, &KMyMoneyRegister::Register::transactionsSelected, q, &KGlobalLedgerView::slotUpdateSummaryLine);
        q->connect(m_register->horizontalHeader(), &QWidget::customContextMenuRequested, q, &KGlobalLedgerView::slotSortOptions);
        q->connect(m_register, &KMyMoneyRegister::Register::reconcileStateColumnClicked, q, &KGlobalLedgerView::slotToggleTransactionMark);

        // insert search line widget

        m_registerSearchLine = new KMyMoneyRegister::RegisterSearchLineWidget(m_register, m_toolbarFrame);
        toolbarLayout->addWidget(m_registerSearchLine);
        toolbarLayout->setStretchFactor(m_registerSearchLine, 100);
        // create the summary frame
        m_summaryFrame = new QFrame();
        QHBoxLayout* summaryFrameLayout = new QHBoxLayout(m_summaryFrame);
        summaryFrameLayout->setContentsMargins(0, 0, 0, 0);
        summaryFrameLayout->setSpacing(0);
        m_leftSummaryLabel = new QLabel(m_summaryFrame);
        m_centerSummaryLabel = new QLabel(m_summaryFrame);
        m_rightSummaryLabel = new QLabel(m_summaryFrame);
        summaryFrameLayout->addWidget(m_leftSummaryLabel);
        QSpacerItem* spacer = new QSpacerItem(20, 1, QSizePolicy::Expanding, QSizePolicy::Minimum);
        summaryFrameLayout->addItem(spacer);
        summaryFrameLayout->addWidget(m_centerSummaryLabel);
        spacer = new QSpacerItem(20, 1, QSizePolicy::Expanding, QSizePolicy::Minimum);
        summaryFrameLayout->addItem(spacer);
        summaryFrameLayout->addWidget(m_rightSummaryLabel);
        vbox->addWidget(m_summaryFrame);

        // create the button frame
        m_buttonFrame = new QFrame(q);
        QVBoxLayout* buttonLayout = new QVBoxLayout(m_buttonFrame);
        buttonLayout->setContentsMargins(0, 0, 0, 0);
        buttonLayout->setSpacing(0);
        vbox->addWidget(m_buttonFrame);
        m_buttonbar = new KToolBar(m_buttonFrame, 0, true);
        m_buttonbar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
        buttonLayout->addWidget(m_buttonbar);

        m_buttonbar->addAction(pActions[eMenu::Action::NewTransaction]);
        m_buttonbar->addAction(pActions[eMenu::Action::DeleteTransaction]);
        m_buttonbar->addAction(pActions[eMenu::Action::EditTransaction]);
        m_buttonbar->addAction(pActions[eMenu::Action::EnterTransaction]);
        m_buttonbar->addAction(pActions[eMenu::Action::CancelTransaction]);
        m_buttonbar->addAction(pActions[eMenu::Action::AcceptTransaction]);
        m_buttonbar->addAction(pActions[eMenu::Action::MatchTransaction]);

        // create the transaction form frame
        m_formFrame = new QFrame(q);
        QVBoxLayout* frameLayout = new QVBoxLayout(m_formFrame);
        frameLayout->setContentsMargins(5, 5, 5, 5);
        frameLayout->setSpacing(0);
        m_form = new KMyMoneyTransactionForm::TransactionForm(m_formFrame);
        frameLayout->addWidget(m_form->getTabBar(m_formFrame));
        frameLayout->addWidget(m_form);
        m_formFrame->setFrameShape(QFrame::Panel);
        m_formFrame->setFrameShadow(QFrame::Raised);
        vbox->addWidget(m_formFrame);

        q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KGlobalLedgerView::refresh);
        q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KGlobalLedgerView::slotUpdateMoveToAccountMenu);
        q->connect(m_register, static_cast<void (KMyMoneyRegister::Register::*)(KMyMoneyRegister::Transaction *)>(&KMyMoneyRegister::Register::focusChanged), m_form, &KMyMoneyTransactionForm::TransactionForm::slotSetTransaction);
        q->connect(m_register, static_cast<void (KMyMoneyRegister::Register::*)()>(&KMyMoneyRegister::Register::focusChanged), q, &KGlobalLedgerView::updateLedgerActionsInternal);
//    q->connect(m_accountComboBox, &KMyMoneyAccountCombo::accountSelected, q, &KGlobalLedgerView::slotAccountSelected);
        q->connect(m_accountComboBox, &KMyMoneyAccountCombo::accountSelected, q, static_cast<void (KGlobalLedgerView::*)(const QString&)>(&KGlobalLedgerView::slotSelectAccount));
        q->connect(m_accountComboBox, &KMyMoneyAccountCombo::accountSelected, q, &KGlobalLedgerView::slotUpdateMoveToAccountMenu);
        q->connect(m_register, &KMyMoneyRegister::Register::transactionsSelected, q, &KGlobalLedgerView::slotTransactionsSelected);
        q->connect(m_register, &KMyMoneyRegister::Register::transactionsSelected, q, &KGlobalLedgerView::slotUpdateMoveToAccountMenu);
        q->connect(m_register, &KMyMoneyRegister::Register::editTransaction, q, &KGlobalLedgerView::slotEditTransaction);
        q->connect(m_register, &KMyMoneyRegister::Register::emptyItemSelected, q, &KGlobalLedgerView::slotNewTransaction);
        q->connect(m_register, &KMyMoneyRegister::Register::aboutToSelectItem, q, &KGlobalLedgerView::slotAboutToSelectItem);
        q->connect(m_mousePressFilter, &MousePressFilter::mousePressedOnExternalWidget, q, &KGlobalLedgerView::slotCancelOrEnterTransactions);

        q->connect(m_form, &KMyMoneyTransactionForm::TransactionForm::newTransaction, q, static_cast<void (KGlobalLedgerView::*)(eWidgets::eRegister::Action)>(&KGlobalLedgerView::slotNewTransactionForm));

        // setup mouse press filter
        m_mousePressFilter->addWidget(m_formFrame);
        m_mousePressFilter->addWidget(m_buttonFrame);
        m_mousePressFilter->addWidget(m_summaryFrame);
        m_mousePressFilter->addWidget(m_registerFrame);

        m_tooltipPosn = QPoint();
    }

    /**
      * This method reloads the account selection combo box of the
      * view with all asset and liability accounts from the engine.
      * If the account id of the current account held in @p m_accountId is
      * empty or if the referenced account does not exist in the engine,
      * the first account found in the list will be made the current account.
      */
    void loadAccounts()
    {
        const auto file = MyMoneyFile::instance();

        // check if the current account still exists and make it the
        // current account
        if (!m_lastSelectedAccountID.isEmpty()) {
            try {
                m_currentAccount = file->account(m_lastSelectedAccountID);
            } catch (const MyMoneyException &) {
                m_lastSelectedAccountID.clear();
                m_currentAccount = MyMoneyAccount();
                m_accountComboBox->setSelected(QString());
            }
        }

        // TODO: check why the invalidate is needed here
        m_filterProxyModel->invalidate();
        m_filterProxyModel->sort((int)eAccountsModel::Column::Account);
        m_filterProxyModel->setHideClosedAccounts(!KMyMoneySettings::showAllAccounts());
        m_filterProxyModel->setHideEquityAccounts(!KMyMoneySettings::expertMode());
        m_accountComboBox->expandAll();

        if (m_currentAccount.id().isEmpty()) {
            // find the first favorite account
            QModelIndexList list = m_filterProxyModel->match(m_filterProxyModel->index(0, 0),
                                   (int)eAccountsModel::Role::Favorite,
                                   QVariant(true),
                                   1,
                                   Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive));
            if (list.count() > 0) {
                QVariant accountId = list.front().data((int)eAccountsModel::Role::ID);
                if (accountId.isValid()) {
                    m_currentAccount = file->account(accountId.toString());
                }
            }

            if (m_currentAccount.id().isEmpty()) {
                // there are no favorite accounts find any account
                list = m_filterProxyModel->match(m_filterProxyModel->index(0, 0),
                                                 Qt::DisplayRole,
                                                 QVariant(QString("*")),
                                                 -1,
                                                 Qt::MatchFlags(Qt::MatchWildcard | Qt::MatchRecursive));
                for (QModelIndexList::ConstIterator it = list.constBegin(); it != list.constEnd(); ++it) {
                    if (!it->parent().isValid())
                        continue; // skip the top level accounts
                    QVariant accountId = (*it).data((int)eAccountsModel::Role::ID);
                    if (accountId.isValid()) {
                        MyMoneyAccount a = file->account(accountId.toString());
                        if (!a.isInvest() && !a.isClosed()) {
                            m_currentAccount = a;
                            break;
                        }
                    }
                }
            }
        }

        if (!m_currentAccount.id().isEmpty()) {
            m_accountComboBox->setSelected(m_currentAccount.id());
            try {
                m_precision = MyMoneyMoney::denomToPrec(m_currentAccount.fraction());
            } catch (const MyMoneyException &) {
                qDebug("Security %s for account %s not found", qPrintable(m_currentAccount.currencyId()), qPrintable(m_currentAccount.name()));
                m_precision = 2;
            }
        }
    }

    /**
      * This method clears the register, form, transaction list. See @sa m_register,
      * @sa m_transactionList
      */
    void clear()
    {
        // clear current register contents
        m_register->clear();

        // setup header font
        QFont font = KMyMoneySettings::listHeaderFontEx();
        QFontMetrics fm(font);
        int height = fm.lineSpacing() + 6;
        m_register->horizontalHeader()->setMinimumHeight(height);
        m_register->horizontalHeader()->setMaximumHeight(height);
        m_register->horizontalHeader()->setFont(font);

        // setup cell font
        font = KMyMoneySettings::listCellFontEx();
        m_register->setFont(font);

        // clear the form
        m_form->clear();

        // the selected transactions list
        m_transactionList.clear();

        // and the selected account in the combo box
        m_accountComboBox->setSelected(QString());

        // fraction defaults to two digits
        m_precision = 2;
    }

    void loadView()
    {
        MYMONEYTRACER(tracer);
        Q_Q(KGlobalLedgerView);

        // setup form visibility
        m_formFrame->setVisible(KMyMoneySettings::transactionForm());

        // no account selected
//    emit q->objectSelected(MyMoneyAccount());
        // no transaction selected
        KMyMoneyRegister::SelectedTransactions list;
        emit q->selectByVariant(QVariantList {QVariant::fromValue(list)}, eView::Intent::SelectRegisterTransactions);

        QMap<QString, bool> isSelected;
        QString focusItemId;
        QString backUpFocusItemId;  // in case the focus item is removed
        QString anchorItemId;
        QString backUpAnchorItemId; // in case the anchor item is removed

        if (!m_newAccountLoaded) {
            // remember the current selected transactions
            KMyMoneyRegister::RegisterItem* item = m_register->firstItem();
            for (; item; item = item->nextItem()) {
                if (item->isSelected()) {
                    isSelected[item->id()] = true;
                }
            }
            // remember the item that has the focus
            storeId(m_register->focusItem(), focusItemId, backUpFocusItemId);
            // and the one that has the selection anchor
            storeId(m_register->anchorItem(), anchorItemId, backUpAnchorItemId);
        } else {
            m_registerSearchLine->searchLine()->clear();
        }

        // clear the current contents ...
        clear();

        // ... load the combobox widget and select current account ...
        loadAccounts();

        // ... setup the register columns ...
        m_register->setupRegister(m_currentAccount);

        // ... setup the form ...
        m_form->setupForm(m_currentAccount);

        if (m_currentAccount.id().isEmpty()) {
            // if we don't have an account we bail out
            q->setEnabled(false);
            return;
        }
        q->setEnabled(true);

        m_register->setUpdatesEnabled(false);

        // ... and recreate it
        KMyMoneyRegister::RegisterItem* focusItem = 0;
        KMyMoneyRegister::RegisterItem* anchorItem = 0;
        QMap<QString, MyMoneyMoney> actBalance, clearedBalance, futureBalance;
        QMap<QString, MyMoneyMoney>::iterator it_b;
        try {
            // setup the filter to select the transactions we want to display
            // and update the sort order
            QString sortOrder;
            QString key;
            QDate reconciliationDate = m_reconciliationDate;

            MyMoneyTransactionFilter filter(m_currentAccount.id());
            // if it's an investment account, we also take care of
            // the sub-accounts (stock accounts)
            if (m_currentAccount.accountType() == eMyMoney::Account::Type::Investment)
                filter.addAccount(m_currentAccount.accountList());

            if (isReconciliationAccount()) {
                key = "kmm-sort-reconcile";
                sortOrder = KMyMoneySettings::sortReconcileView();
                filter.addState((int)eMyMoney::TransactionFilter::State::NotReconciled);
                filter.addState((int)eMyMoney::TransactionFilter::State::Cleared);
            } else {
                filter.setDateFilter(KMyMoneySettings::startDate().date(), QDate());
                key = "kmm-sort-std";
                sortOrder = KMyMoneySettings::sortNormalView();
                if (KMyMoneySettings::hideReconciledTransactions()
                        && !m_currentAccount.isIncomeExpense()) {
                    filter.addState((int)eMyMoney::TransactionFilter::State::NotReconciled);
                    filter.addState((int)eMyMoney::TransactionFilter::State::Cleared);
                }
            }
            filter.setReportAllSplits(true);

            // check if we have an account override of the sort order
            if (!m_currentAccount.value(key).isEmpty())
                sortOrder = m_currentAccount.value(key);

            // setup sort order
            m_register->setSortOrder(sortOrder);

            // retrieve the list from the engine
            MyMoneyFile::instance()->transactionList(m_transactionList, filter);

            emit q->slotStatusProgress(0, m_transactionList.count());

            // create the elements for the register
            QList<QPair<MyMoneyTransaction, MyMoneySplit> >::const_iterator it;
            QMap<QString, int>uniqueMap;
            int i = 0;
            for (it = m_transactionList.constBegin(); it != m_transactionList.constEnd(); ++it) {
                uniqueMap[(*it).first.id()]++;
                KMyMoneyRegister::Transaction* t = KMyMoneyRegister::Register::transactionFactory(m_register, (*it).first, (*it).second, uniqueMap[(*it).first.id()]);
                actBalance[t->split().accountId()] = MyMoneyMoney();
                emit q->slotStatusProgress(++i, 0);
                // if we're in reconciliation and the state is cleared, we
                // force the item to show in dimmed intensity to get a visual focus
                // on those items, that we need to work on
                if (isReconciliationAccount() && (*it).second.reconcileFlag() == eMyMoney::Split::State::Cleared) {
                    t->setReducedIntensity(true);
                }
            }

            // create dummy entries for the scheduled transactions if sorted by postdate
            int period = KMyMoneySettings::schedulePreview();
            if (m_register->primarySortKey() == eWidgets::SortField::PostDate) {
                // show scheduled transactions which have a scheduled postdate
                // within the next 'period' days. In reconciliation mode, the
                // period starts on the statement date.
                QDate endDate = QDate::currentDate().addDays(period);
                if (isReconciliationAccount())
                    endDate = reconciliationDate.addDays(period);
                QList<MyMoneySchedule> scheduleList = MyMoneyFile::instance()->scheduleList(m_currentAccount.id());
                while (!scheduleList.isEmpty()) {
                    MyMoneySchedule& s = scheduleList.first();
                    for (;;) {
                        if (s.isFinished() || s.adjustedNextDueDate() > endDate) {
                            break;
                        }

                        MyMoneyTransaction t(s.id(), KMyMoneyUtils::scheduledTransaction(s));
                        if (s.isOverdue() && !KMyMoneySettings::showPlannedScheduleDates()) {
                            // if the transaction is scheduled and overdue, it can't
                            // certainly be posted in the past. So we take today's date
                            // as the alternative
                            t.setPostDate(s.adjustedDate(QDate::currentDate(), s.weekendOption()));
                        } else {
                            t.setPostDate(s.adjustedNextDueDate());
                        }
                        foreach (const auto split, t.splits()) {
                            if (split.accountId() == m_currentAccount.id()) {
                                new KMyMoneyRegister::StdTransactionScheduled(m_register, t, split, uniqueMap[t.id()]);
                            }
                        }
                        // keep track of this payment locally (not in the engine)
                        if (s.isOverdue() && !KMyMoneySettings::showPlannedScheduleDates()) {
                            s.setLastPayment(QDate::currentDate());
                        } else {
                            s.setLastPayment(s.nextDueDate());
                        }

                        // if this is a one time schedule, we can bail out here as we're done
                        if (s.occurrence() == eMyMoney::Schedule::Occurrence::Once)
                            break;

                        // for all others, we check if the next payment date is still 'in range'
                        QDate nextDueDate = s.nextPayment(s.nextDueDate());
                        if (nextDueDate.isValid()) {
                            s.setNextDueDate(nextDueDate);
                        } else {
                            break;
                        }
                    }
                    scheduleList.pop_front();
                }
            }

            // add the group markers
            m_register->addGroupMarkers();

            // sort the transactions according to the sort setting
            m_register->sortItems();

            // remove trailing and adjacent markers
            m_register->removeUnwantedGroupMarkers();

            // add special markers for reconciliation now so that they do not get
            // removed by m_register->removeUnwantedGroupMarkers(). Needs resorting
            // of items but that's ok.

            KMyMoneyRegister::StatementGroupMarker* statement = 0;
            KMyMoneyRegister::StatementGroupMarker* dStatement = 0;
            KMyMoneyRegister::StatementGroupMarker* pStatement = 0;

            if (isReconciliationAccount()) {
                switch (m_register->primarySortKey()) {
                case eWidgets::SortField::PostDate:
                    statement = new KMyMoneyRegister::StatementGroupMarker(m_register, eWidgets::eRegister::CashFlowDirection::Deposit, reconciliationDate, i18n("Statement Details"));
                    m_register->sortItems();
                    break;
                case eWidgets::SortField::Type:
                    dStatement = new KMyMoneyRegister::StatementGroupMarker(m_register, eWidgets::eRegister::CashFlowDirection::Deposit, reconciliationDate, i18n("Statement Deposit Details"));
                    pStatement = new KMyMoneyRegister::StatementGroupMarker(m_register, eWidgets::eRegister::CashFlowDirection::Payment, reconciliationDate, i18n("Statement Payment Details"));
                    m_register->sortItems();
                    break;
                default:
                    break;
                }
            }

            // we need at least the balance for the account we currently show
            actBalance[m_currentAccount.id()] = MyMoneyMoney();

            if (m_currentAccount.accountType() == eMyMoney::Account::Type::Investment)
                foreach (const auto accountID, m_currentAccount.accountList())
                    actBalance[accountID] = MyMoneyMoney();

            // determine balances (actual, cleared). We do this by getting the actual
            // balance of all entered transactions from the engine and walk the list
            // of transactions backward. Also re-select a transaction if it was
            // selected before and setup the focus item.

            MyMoneyMoney factor(1, 1);
            if (m_currentAccount.accountGroup() == eMyMoney::Account::Type::Liability
                    || m_currentAccount.accountGroup() == eMyMoney::Account::Type::Equity)
                factor = -factor;

            QMap<QString, int> deposits;
            QMap<QString, int> payments;
            QMap<QString, MyMoneyMoney> depositAmount;
            QMap<QString, MyMoneyMoney> paymentAmount;
            for (it_b = actBalance.begin(); it_b != actBalance.end(); ++it_b) {
                MyMoneyMoney balance = MyMoneyFile::instance()->balance(it_b.key());
                balance = balance * factor;
                clearedBalance[it_b.key()] =
                    futureBalance[it_b.key()] =
                        (*it_b) = balance;
                deposits[it_b.key()] = payments[it_b.key()] = 0;
                depositAmount[it_b.key()] = MyMoneyMoney();
                paymentAmount[it_b.key()] = MyMoneyMoney();
            }

            tracer.printf("total balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(actBalance[m_currentAccount.id()].formatMoney("", 2)));
            tracer.printf("future balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(futureBalance[m_currentAccount.id()].formatMoney("", 2)));
            tracer.printf("cleared balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(clearedBalance[m_currentAccount.id()].formatMoney("", 2)));

            KMyMoneyRegister::RegisterItem* p = m_register->lastItem();
            focusItem = 0;

            // take care of possibly trailing scheduled transactions (bump up the future balance)
            while (p) {
                if (p->isSelectable()) {
                    KMyMoneyRegister::Transaction* t = dynamic_cast<KMyMoneyRegister::Transaction*>(p);
                    if (t && t->isScheduled()) {
                        MyMoneyMoney balance = futureBalance[t->split().accountId()];
                        const MyMoneySplit& split = t->split();
                        // if this split is a stock split, we can't just add the amount of shares
                        if (t->transaction().isStockSplit()) {
                            balance = MyMoneyFile::instance()->stockSplit(split.accountId(),
                                                                          balance,
                                                                          split.shares(),
                                                                          eMyMoney::StockSplitDirection::StockSplitForward);
                        } else {
                            balance += split.shares() * factor;
                        }
                        futureBalance[split.accountId()] = balance;
                    } else if (t && !focusItem)
                        focusItem = p;
                }
                p = p->prevItem();
            }

            p = m_register->lastItem();
            while (p) {
                KMyMoneyRegister::Transaction* t = dynamic_cast<KMyMoneyRegister::Transaction*>(p);
                if (t) {
                    if (isSelected.contains(t->id()))
                        t->setSelected(true);

                    matchItemById(&focusItem, t, focusItemId, backUpFocusItemId);
                    matchItemById(&anchorItem, t, anchorItemId, backUpAnchorItemId);

                    const MyMoneySplit& split = t->split();
                    MyMoneyMoney balance = futureBalance[split.accountId()];
                    t->setBalance(balance);

                    // if this split is a stock split, we can't just add the amount of shares
                    if (t->transaction().isStockSplit()) {
                        balance =
                            MyMoneyFile::instance()->stockSplit(split.accountId(), balance, split.shares(), eMyMoney::StockSplitDirection::StockSplitBackward);
                    } else {
                        balance -= split.shares() * factor;
                    }

                    if (!t->isScheduled()) {
                        if (isReconciliationAccount() && t->transaction().postDate() <= reconciliationDate && split.reconcileFlag() == eMyMoney::Split::State::Cleared) {
                            if (split.shares().isNegative()) {
                                payments[split.accountId()]++;
                                paymentAmount[split.accountId()] += split.shares();
                            } else {
                                deposits[split.accountId()]++;
                                depositAmount[split.accountId()] += split.shares();
                            }
                        }

                        if (t->transaction().postDate() > QDate::currentDate()) {
                            tracer.printf("Reducing actual balance by %s because %s/%s(%s) is in the future", qPrintable((split.shares() * factor).formatMoney("", 2)), qPrintable(t->transaction().id()), qPrintable(split.id()), qPrintable(t->transaction().postDate().toString(Qt::ISODate)));
                            actBalance[split.accountId()] -= split.shares() * factor;
                        }
                    }
                    futureBalance[split.accountId()] = balance;
                }
                p = p->prevItem();
            }

            clearedBalance[m_currentAccount.id()] = MyMoneyFile::instance()->clearedBalance(m_currentAccount.id(), reconciliationDate);

            tracer.printf("total balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(actBalance[m_currentAccount.id()].formatMoney("", 2)));
            tracer.printf("future balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(futureBalance[m_currentAccount.id()].formatMoney("", 2)));
            tracer.printf("cleared balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(clearedBalance[m_currentAccount.id()].formatMoney("", 2)));

            // update statement information
            if (statement) {
                const QString aboutDeposits = i18np("%1 deposit (%2)", "%1 deposits (%2)",
                                                    deposits[m_currentAccount.id()], depositAmount[m_currentAccount.id()].abs().formatMoney(m_currentAccount.fraction()));
                const QString aboutCharges = i18np("%1 charge (%2)", "%1 charges (%2)",
                                                   deposits[m_currentAccount.id()], depositAmount[m_currentAccount.id()].abs().formatMoney(m_currentAccount.fraction()));
                const QString aboutPayments = i18np("%1 payment (%2)", "%1 payments (%2)",
                                                    payments[m_currentAccount.id()], paymentAmount[m_currentAccount.id()].abs().formatMoney(m_currentAccount.fraction()));

                statement->setText(i18nc("%1 is a string, e.g. 7 deposits; %2 is a string, e.g. 4 payments", "%1, %2",
                                         m_currentAccount.accountType() == eMyMoney::Account::Type::CreditCard ? aboutCharges : aboutDeposits,
                                         aboutPayments));
            }
            if (pStatement) {
                pStatement->setText(i18np("%1 payment (%2)", "%1 payments (%2)", payments[m_currentAccount.id()]
                                          , paymentAmount[m_currentAccount.id()].abs().formatMoney(m_currentAccount.fraction())));
            }
            if (dStatement) {
                dStatement->setText(i18np("%1 deposit (%2)", "%1 deposits (%2)", deposits[m_currentAccount.id()]
                                          , depositAmount[m_currentAccount.id()].abs().formatMoney(m_currentAccount.fraction())));
            }

            // add a last empty entry for new transactions
            // leave some information about the current account
            MyMoneySplit split;
            split.setReconcileFlag(eMyMoney::Split::State::NotReconciled);
            // make sure to use the value specified in the option during reconciliation
            if (isReconciliationAccount())
                split.setReconcileFlag(static_cast<eMyMoney::Split::State>(KMyMoneySettings::defaultReconciliationState()));
            MyMoneyTransaction emptyTransaction;
            emptyTransaction.setCommodity(m_currentAccount.currencyId());
            KMyMoneyRegister::Register::transactionFactory(m_register, emptyTransaction, split, 0);

            m_register->updateRegister(true);

            if (focusItem) {
                // in case we have some selected items we just set the focus item
                // in other cases, we make the focusitem also the selected item
                if (anchorItem && (anchorItem != focusItem)) {
                    m_register->setFocusItem(focusItem);
                    m_register->setAnchorItem(anchorItem);
                } else
                    m_register->selectItem(focusItem, true);
            } else {
                // just use the empty line at the end if nothing else exists in the ledger
                p = m_register->lastItem();
                m_register->setFocusItem(p);
                m_register->selectItem(p);
                focusItem = p;
            }

            updateSummaryLine(actBalance, clearedBalance);
            emit q->slotStatusProgress(-1, -1);

        } catch (const MyMoneyException &) {
            m_currentAccount = MyMoneyAccount();
            clear();
        }

        m_showDetails = KMyMoneySettings::showRegisterDetailed();

        // and tell everyone what's selected
        emit q->selectByObject(m_currentAccount, eView::Intent::None);
        KMyMoneyRegister::SelectedTransactions actualSelection(m_register);
        emit q->selectByVariant(QVariantList {QVariant::fromValue(actualSelection)}, eView::Intent::SelectRegisterTransactions);
    }

    void selectTransaction(const QString& id)
    {
        if (!id.isEmpty()) {
            KMyMoneyRegister::RegisterItem* p = m_register->lastItem();
            while (p) {
                KMyMoneyRegister::Transaction* t = dynamic_cast<KMyMoneyRegister::Transaction*>(p);
                if (t) {
                    if (t->transaction().id() == id) {
                        m_register->selectItem(t);
                        m_register->ensureItemVisible(t);
                        break;
                    }
                }
                p = p->prevItem();
            }
        }
    }

    /**
     * @brief selects transactions for processing with slots
     * @param list of transactions
     * @return false if only schedule is to be selected
     */
    bool selectTransactions(const KMyMoneyRegister::SelectedTransactions list)
    {
        Q_Q(KGlobalLedgerView);
        // list can either contain a list of transactions or a single selected scheduled transaction
        // in the latter case, the transaction id is actually the one of the schedule. In order
        // to differentiate between the two, we just ask for the schedule. If we don't find one - because
        // we passed the id of a real transaction - then we know that fact.  We use the schedule here,
        // because the list of schedules is kept in a cache by MyMoneyFile. This way, we save some trips
        // to the backend which we would have to do if we check for the transaction.
        m_selectedTransactions.clear();
        auto sch = MyMoneySchedule();
        auto ret = true;

        m_accountGoto.clear();
        m_payeeGoto.clear();
        if (!list.isEmpty() && !list.first().isScheduled()) {
            m_selectedTransactions = list;
            if (list.count() == 1) {
                const MyMoneySplit& sp = m_selectedTransactions[0].split();
                if (!sp.payeeId().isEmpty()) {
                    try {
                        auto payee = MyMoneyFile::instance()->payee(sp.payeeId());
                        if (!payee.name().isEmpty()) {
                            m_payeeGoto = payee.id();
                            auto name = payee.name();
                            name.replace(QRegExp("&(?!&)"), "&&");
                            pActions[Action::GoToPayee]->setText(i18n("Go to '%1'", name));
                        }
                    } catch (const MyMoneyException &) {
                    }
                }
                try {
                    const auto& t = m_selectedTransactions[0].transaction();
                    // search the first non-income/non-expense account and use it for the 'goto account'
                    const auto& selectedTransactionSplit = m_selectedTransactions[0].split();
                    foreach (const auto split, t.splits()) {
                        if (split.id() != selectedTransactionSplit.id()) {
                            auto acc = MyMoneyFile::instance()->account(split.accountId());

                            // set default icon
                            pActions[Action::GoToAccount]->setIcon(Icons::get(Icons::Icon::Accounts));

                            // for stock accounts we show the portfolio account
                            if (acc.isInvest()) {
                                acc = MyMoneyFile::instance()->account(acc.parentAccountId());
                            } else if (acc.isIncomeExpense()) {
                                pActions[Action::GoToAccount]->setIcon(Icons::get(Icons::Icon::FinancialCategories));
                            }

                            m_accountGoto = acc.id();
                            auto name = acc.name();
                            name.replace(QRegExp("&(?!&)"), "&&");
                            pActions[Action::GoToAccount]->setText(i18n("Go to '%1'", name));

                            break;
                        }
                    }
                } catch (const MyMoneyException &) {
                }
            }
        } else if (!list.isEmpty()) {
            sch = MyMoneyFile::instance()->schedule(list.first().scheduleId());
            m_selectedTransactions.append(list.first());
            ret = false;
        }

        emit q->selectByObject(sch, eView::Intent::None);

        // make sure, we show some neutral menu entry if we don't have an object
        if (m_payeeGoto.isEmpty())
            pActions[Action::GoToPayee]->setText(i18n("Go to payee"));
        if (m_accountGoto.isEmpty())
            pActions[Action::GoToAccount]->setText(i18n("Go to account"));
        return ret;
    }

    /**
      * Returns @a true if setReconciliationAccount() has been called for
      * the current loaded account.
      *
      * @retval true current account is in reconciliation mode
      * @retval false current account is not in reconciliation mode
      */
    bool isReconciliationAccount() const
    {
        return m_currentAccount.id() == m_reconciliationAccount.id();
    }

    /**
      * Updates the values on the summary line beneath the register with
      * the given values. The contents shown differs between reconciliation
      * mode and normal mode.
      *
      * @param actBalance map of account indexed values to be used as actual balance
      * @param clearedBalance map of account indexed values to be used as cleared balance
      */
    void updateSummaryLine(const QMap<QString, MyMoneyMoney>& actBalance, const QMap<QString, MyMoneyMoney>& clearedBalance)
    {
        Q_Q(KGlobalLedgerView);
        const auto file = MyMoneyFile::instance();
        m_leftSummaryLabel->show();
        m_centerSummaryLabel->show();
        m_rightSummaryLabel->show();

        if (isReconciliationAccount()) {
            if (m_currentAccount.accountType() != eMyMoney::Account::Type::Investment) {
                m_leftSummaryLabel->setText(i18n("Statement: %1", m_endingBalance.formatMoney("", m_precision)));
                m_centerSummaryLabel->setText(i18nc("Cleared balance", "Cleared: %1", clearedBalance[m_currentAccount.id()].formatMoney("", m_precision)));
                m_totalBalance = clearedBalance[m_currentAccount.id()] - m_endingBalance;
            }
        } else {
            // update summary line in normal mode
            QDate reconcileDate = m_currentAccount.lastReconciliationDate();

            if (reconcileDate.isValid()) {
                m_leftSummaryLabel->setText(i18n("Last reconciled: %1", QLocale().toString(reconcileDate, QLocale::ShortFormat)));
            } else {
                m_leftSummaryLabel->setText(i18n("Never reconciled"));
            }

            QPalette palette = m_rightSummaryLabel->palette();
            palette.setColor(m_rightSummaryLabel->foregroundRole(), m_leftSummaryLabel->palette().color(q->foregroundRole()));
            if (m_currentAccount.accountType() != eMyMoney::Account::Type::Investment) {
                m_centerSummaryLabel->setText(i18nc("Cleared balance", "Cleared: %1", clearedBalance[m_currentAccount.id()].formatMoney("", m_precision)));
                m_totalBalance = actBalance[m_currentAccount.id()];
            } else {
                m_centerSummaryLabel->hide();
                MyMoneyMoney balance;
                MyMoneySecurity base = file->baseCurrency();
                QMap<QString, MyMoneyMoney>::const_iterator it_b;
                // reset the approximated flag
                m_balanceIsApproximated = false;
                for (it_b = actBalance.begin(); it_b != actBalance.end(); ++it_b) {
                    MyMoneyAccount stock = file->account(it_b.key());
                    QString currencyId = stock.currencyId();
                    MyMoneySecurity sec = file->security(currencyId);
                    MyMoneyMoney rate(1, 1);

                    if (stock.isInvest()) {
                        currencyId = sec.tradingCurrency();
                        const MyMoneyPrice &priceInfo = file->price(sec.id(), currencyId);
                        m_balanceIsApproximated |= !priceInfo.isValid();
                        rate = priceInfo.rate(sec.tradingCurrency());
                    }

                    if (currencyId != base.id()) {
                        const MyMoneyPrice &priceInfo = file->price(sec.tradingCurrency(), base.id());
                        m_balanceIsApproximated |= !priceInfo.isValid();
                        rate = (rate * priceInfo.rate(base.id())).convertPrecision(sec.pricePrecision());
                    }
                    balance += ((*it_b) * rate).convert(base.smallestAccountFraction());
                }
                m_totalBalance = balance;
            }
            m_rightSummaryLabel->setPalette(palette);
        }
        // determine the number of selected transactions
        KMyMoneyRegister::SelectedTransactions selection;
        m_register->selectedTransactions(selection);
        q->slotUpdateSummaryLine(selection);
    }

    /**
      * setup the default action according to the current account type
      */
    void setupDefaultAction()
    {
        switch (m_currentAccount.accountType()) {
        case eMyMoney::Account::Type::Asset:
        case eMyMoney::Account::Type::AssetLoan:
        case eMyMoney::Account::Type::Savings:
            m_action = eWidgets::eRegister::Action::Deposit;
            break;
        default:
            m_action = eWidgets::eRegister::Action::Withdrawal;
            break;
        }
    }

    // used to store the id of an item and the id of an immediate unselected sibling
    void storeId(KMyMoneyRegister::RegisterItem *item, QString &id, QString &backupId) {
        if (item) {
            // the id of the item
            id = item->id();
            // the id of the item's previous/next unselected item
            for (KMyMoneyRegister::RegisterItem *it = item->prevItem(); it != 0 && backupId.isEmpty(); it = it->prevItem()) {
                if (!it->isSelected()) {
                    backupId = it->id();
                }
            }
            // if we didn't found previous unselected items search trough the next items
            for (KMyMoneyRegister::RegisterItem *it = item->nextItem(); it != 0 && backupId.isEmpty(); it = it->nextItem()) {
                if (!it->isSelected()) {
                    backupId = it->id();
                }
            }
        }
    }

    // use to match an item by it's id or a backup id which has a lower precedence
    void matchItemById(KMyMoneyRegister::RegisterItem **item, KMyMoneyRegister::Transaction* t, QString &id, QString &backupId) {
        if (!backupId.isEmpty() && t->id() == backupId)
            *item = t;
        if (!id.isEmpty() && t->id() == id) {
            // we found the real thing there's no need for the backup anymore
            backupId.clear();
            *item = t;
        }
    }

    bool canProcessTransactions(const KMyMoneyRegister::SelectedTransactions& list, QString& tooltip) const
    {
        if (m_register->focusItem() == 0)
            return false;

        if (!m_register->focusItem()->isSelected()) {
            tooltip = i18n("Cannot process transaction with focus if it is not selected.");
            showTooltip(tooltip);
            return false;
        }
        tooltip.clear();
        return !list.isEmpty();
    }

    void showTooltip(const QString msg) const
    {
        QToolTip::showText(m_tooltipPosn, msg);
    }

    bool createNewTransaction()
    {
        Q_Q(KGlobalLedgerView);
        auto rc = false;
        QString txt;
        if (q->canCreateTransactions(txt)) {
            rc = q->selectEmptyTransaction();
        }
        return rc;
    }

    TransactionEditor* startEdit(const KMyMoneyRegister::SelectedTransactions& list)
    {
        Q_Q(KGlobalLedgerView);
        TransactionEditor* editor = 0;
        QString txt;
        if (q->canEditTransactions(list, txt) || q->canCreateTransactions(txt)) {
            editor = q->startEdit(list);
        }
        return editor;
    }

    void doDeleteTransactions()
    {
        Q_Q(KGlobalLedgerView);
        KMyMoneyRegister::SelectedTransactions list = m_selectedTransactions;
        KMyMoneyRegister::SelectedTransactions::iterator it_t;
        int cnt = list.count();
        int i = 0;
        emit q->slotStatusProgress(0, cnt);
        MyMoneyFileTransaction ft;
        const auto file = MyMoneyFile::instance();
        try {
            it_t = list.begin();
            while (it_t != list.end()) {
                const auto accountId = (*it_t).split().accountId();
                const auto deletedNum = (*it_t).split().number();
                const auto transactionId = (*it_t).transaction().id();
                // only remove those transactions that do not reference a closed account
                if (!file->referencesClosedAccount((*it_t).transaction())) {
                    file->removeTransaction((*it_t).transaction());
                    // remove all those references in the list of selected transactions
                    // that refer to the same transaction we just removed so that we
                    // will not be caught by an exception later on (see bko #285310)
                    while (it_t != list.end()) {
                        if (transactionId == (*it_t).transaction().id()) {
                            it_t = list.erase(it_t);
                            i++; // bump count of deleted transactions
                        } else {
                            ++it_t;
                        }
                    }

                } else {
                    list.erase(it_t);
                }
                it_t = list.begin();

                // need to ensure "nextCheckNumber" is still correct
                auto acc = file->account(accountId);

                // the "lastNumberUsed" might have been the txn number deleted
                // so adjust it
                if (deletedNum == acc.value("lastNumberUsed")) {
                    // decrement deletedNum and set new "lastNumberUsed"
                    QString num = KMyMoneyUtils::getAdjacentNumber(deletedNum, -1);
                    acc.setValue("lastNumberUsed", num);
                    file->modifyAccount(acc);
                }

                emit q->slotStatusProgress(i++, 0);
            }
            ft.commit();

        } catch (const MyMoneyException &e) {
            KMessageBox::detailedSorry(q, i18n("Unable to delete transaction(s)"), e.what());
        }
        emit q->slotStatusProgress(-1, -1);
    }

    void deleteTransactionEditor()
    {
        // make sure, we don't use the transaction editor pointer
        // anymore from now on
        auto p = m_transactionEditor;
        m_transactionEditor = nullptr;
        delete p;
    }

    void transactionUnmatch()
    {
        Q_Q(KGlobalLedgerView);
        KMyMoneyRegister::SelectedTransactions::const_iterator it;
        MyMoneyFileTransaction ft;
        try {
            for (it = m_selectedTransactions.constBegin(); it != m_selectedTransactions.constEnd(); ++it) {
                if ((*it).split().isMatched()) {
                    TransactionMatcher matcher(m_currentAccount);
                    matcher.unmatch((*it).transaction(), (*it).split());
                }
            }
            ft.commit();

        } catch (const MyMoneyException &e) {
            KMessageBox::detailedSorry(q, i18n("Unable to unmatch the selected transactions"), e.what());
        }
    }

    void transactionMatch()
    {
        Q_Q(KGlobalLedgerView);
        if (m_selectedTransactions.count() != 2)
            return;

        KMyMoneyRegister::SelectedTransaction toBeDeleted;
        KMyMoneyRegister::SelectedTransaction remaining;
        for (const auto& it : m_selectedTransactions) {
            if (it.transaction().isImported()) {
                if (toBeDeleted.transaction().id().isEmpty()) {
                    toBeDeleted = it;
                } else {
                    //This is a second imported transaction, we still want to merge
                    remaining = it;
                }
            } else if (!it.split().isMatched()) {
                if (remaining.transaction().id().isEmpty()) {
                    remaining = it;
                } else {
                    toBeDeleted = it;
                }
            }
        }

        // the user selected two transactions but they might be
        // selected in the wrong order. We check here, if the
        // other way around makes more sense and simply exchange
        // the two. See bko #435512
        if ((remaining.transaction().splitCount() == 1) && (toBeDeleted.transaction().splitCount() > 1)) {
            swap(remaining, toBeDeleted);
        }

        bool doMatch = true;
        // in case the transaction we are about to remove contains
        // more than one split, we ask the user if this is what
        // she really wants.
        if (toBeDeleted.transaction().splitCount() > 1) {
            KMergeTransactionsDlg dlg(m_currentAccount);
            dlg.addTransaction(remaining.transaction());
            dlg.addTransaction(toBeDeleted.transaction());
            doMatch = (dlg.exec() == QDialog::Accepted);
        }
        if (doMatch) {
            MyMoneyFileTransaction ft;
            try {
                if (remaining.transaction().id().isEmpty())
                    throw MYMONEYEXCEPTION(QString::fromLatin1("No manually entered transaction selected for matching"));
                if (toBeDeleted.transaction().id().isEmpty())
                    throw MYMONEYEXCEPTION(QString::fromLatin1("No imported transaction selected for matching"));

                TransactionMatcher matcher(m_currentAccount);
                matcher.match(remaining.transaction(), remaining.split(), toBeDeleted.transaction(), toBeDeleted.split(), true);
                ft.commit();
            } catch (const MyMoneyException &e) {
                KMessageBox::detailedSorry(q, i18n("Unable to match the selected transactions"), e.what());
            }
        }
    }

    /**
      * Mark the selected transactions as provided by @a flag. If
      * flag is @a MyMoneySplit::Unknown, the future state depends
      * on the current stat of the split's flag according to the
      * following table:
      *
      * - NotReconciled --> Cleared
      * - Cleared --> Reconciled
      * - Reconciled --> NotReconciled
      */
    void markTransaction(eMyMoney::Split::State flag)
    {
        Q_Q(KGlobalLedgerView);
        auto list = m_selectedTransactions;
        KMyMoneyRegister::SelectedTransactions::const_iterator it_t;
        auto cnt = list.count();
        auto i = 0;
        emit q->slotStatusProgress(0, cnt);
        MyMoneyFileTransaction ft;
        try {
            for (it_t = list.constBegin(); it_t != list.constEnd(); ++it_t) {
                // turn on signals before we modify the last entry in the list
                cnt--;
                MyMoneyFile::instance()->blockSignals(cnt != 0);

                // get a fresh copy
                auto t = MyMoneyFile::instance()->transaction((*it_t).transaction().id());
                auto sp = t.splitById((*it_t).split().id());
                if (sp.reconcileFlag() != flag) {
                    if (flag == eMyMoney::Split::State::Unknown) {
                        if (m_reconciliationAccount.id().isEmpty()) {
                            // in normal mode we cycle through all states
                            switch (sp.reconcileFlag()) {
                            case eMyMoney::Split::State::NotReconciled:
                                sp.setReconcileFlag(eMyMoney::Split::State::Cleared);
                                break;
                            case eMyMoney::Split::State::Cleared:
                                sp.setReconcileFlag(eMyMoney::Split::State::Reconciled);
                                break;
                            case eMyMoney::Split::State::Reconciled:
                                sp.setReconcileFlag(eMyMoney::Split::State::NotReconciled);
                                break;
                            default:
                                break;
                            }
                        } else {
                            // in reconciliation mode we skip the reconciled state
                            switch (sp.reconcileFlag()) {
                            case eMyMoney::Split::State::NotReconciled:
                                sp.setReconcileFlag(eMyMoney::Split::State::Cleared);
                                break;
                            case eMyMoney::Split::State::Cleared:
                                sp.setReconcileFlag(eMyMoney::Split::State::NotReconciled);
                                break;
                            default:
                                break;
                            }
                        }
                    } else {
                        sp.setReconcileFlag(flag);
                    }

                    t.modifySplit(sp);
                    MyMoneyFile::instance()->modifyTransaction(t);
                }
                emit q->slotStatusProgress(i++, 0);
            }
            emit q->slotStatusProgress(-1, -1);
            ft.commit();
        } catch (const MyMoneyException &e) {
            KMessageBox::detailedSorry(q, i18n("Unable to modify transaction"), e.what());
        }
    }

    // move a stock transaction from one investment account to another
    void moveInvestmentTransaction(const QString& /*fromId*/,
                                   const QString& toId,
                                   const MyMoneyTransaction& tx)
    {
        MyMoneyAccount toInvAcc = MyMoneyFile::instance()->account(toId);
        MyMoneyTransaction t(tx);
        // first determine which stock we are dealing with.
        // fortunately, investment transactions have only one stock involved
        QString stockAccountId;
        QString stockSecurityId;
        MyMoneySplit s;
        foreach (const auto split, t.splits()) {
            stockAccountId = split.accountId();
            stockSecurityId =
                MyMoneyFile::instance()->account(stockAccountId).currencyId();
            if (!MyMoneyFile::instance()->security(stockSecurityId).isCurrency()) {
                s = split;
                break;
            }
        }
        // Now check the target investment account to see if it
        // contains a stock with this id
        QString newStockAccountId;
        foreach (const auto sAccount, toInvAcc.accountList()) {
            if (MyMoneyFile::instance()->account(sAccount).currencyId() ==
                    stockSecurityId) {
                newStockAccountId = sAccount;
                break;
            }
        }
        // if it doesn't exist, we need to add it as a copy of the old one
        // no 'copyAccount()' function??
        if (newStockAccountId.isEmpty()) {
            MyMoneyAccount stockAccount =
                MyMoneyFile::instance()->account(stockAccountId);
            MyMoneyAccount newStock;
            newStock.setName(stockAccount.name());
            newStock.setNumber(stockAccount.number());
            newStock.setDescription(stockAccount.description());
            newStock.setInstitutionId(stockAccount.institutionId());
            newStock.setOpeningDate(stockAccount.openingDate());
            newStock.setAccountType(stockAccount.accountType());
            newStock.setCurrencyId(stockAccount.currencyId());
            newStock.setClosed(stockAccount.isClosed());
            MyMoneyFile::instance()->addAccount(newStock, toInvAcc);
            newStockAccountId = newStock.id();
        }
        // now update the split and the transaction
        s.setAccountId(newStockAccountId);
        t.modifySplit(s);
        MyMoneyFile::instance()->modifyTransaction(t);
    }

    void createTransactionMoveMenu()
    {
        Q_Q(KGlobalLedgerView);
        if (!m_moveToAccountSelector) {
            auto menu = pMenus[eMenu::Menu::MoveTransaction];
            if (menu ) {
                auto accountSelectorAction = new QWidgetAction(menu);
                m_moveToAccountSelector = new KMyMoneyAccountSelector(menu, 0, false);
                m_moveToAccountSelector->setObjectName("transaction_move_menu_selector");
                accountSelectorAction->setDefaultWidget(m_moveToAccountSelector);
                menu->addAction(accountSelectorAction);
                q->connect(m_moveToAccountSelector, &QObject::destroyed, q, &KGlobalLedgerView::slotObjectDestroyed);
                q->connect(m_moveToAccountSelector, &KMyMoneySelector::itemSelected, q, &KGlobalLedgerView::slotMoveToAccount);
            }
        }
    }

    QList<QPair<MyMoneyTransaction, MyMoneySplit> > automaticReconciliation(const MyMoneyAccount &account,
            const QList<QPair<MyMoneyTransaction, MyMoneySplit> > &transactions,
            const MyMoneyMoney &amount)
    {
        Q_Q(KGlobalLedgerView);
        static const int NR_OF_STEPS_LIMIT = 60000;
        static const int PROGRESSBAR_STEPS = 1000;
        QList<QPair<MyMoneyTransaction, MyMoneySplit> > result = transactions;

        // optimize the most common case - all transactions should be cleared
        QListIterator<QPair<MyMoneyTransaction, MyMoneySplit> > itTransactionSplitResult(result);
        MyMoneyMoney transactionsBalance;
        while (itTransactionSplitResult.hasNext()) {
            const QPair<MyMoneyTransaction, MyMoneySplit> &transactionSplit = itTransactionSplitResult.next();
            transactionsBalance += transactionSplit.second.shares();
        }
        if (amount == transactionsBalance) {
            result = transactions;
            return result;
        }

        // only one transaction is uncleared
        itTransactionSplitResult.toFront();
        int index = 0;
        while (itTransactionSplitResult.hasNext()) {
            const QPair<MyMoneyTransaction, MyMoneySplit> &transactionSplit = itTransactionSplitResult.next();
            if (transactionsBalance - transactionSplit.second.shares() == amount) {
                result.removeAt(index);
                return result;
            }
            index++;
        }

        // more than one transaction is uncleared - apply the algorithm
        result.clear();

        const auto& security = MyMoneyFile::instance()->security(account.currencyId());
        double precision = 0.1 / account.fraction(security);

        QList<MyMoneyMoney> sumList;
        sumList << MyMoneyMoney();

        QMap<MyMoneyMoney, QList<QPair<QString, QString> > > sumToComponentsMap;

        struct restoreStatusMsgHelper {
            restoreStatusMsgHelper(KGlobalLedgerView* qq)
                : q(qq) {}

            ~restoreStatusMsgHelper()
            {
                q->slotStatusMsg(QString());
                q->slotStatusProgress(-1, -1);
            }
            KGlobalLedgerView* q;
        } restoreHelper(q);

        q->slotStatusMsg(i18n("Running automatic reconciliation"));
        q->slotStatusProgress(0, NR_OF_STEPS_LIMIT);

        // compute the possible matches
        QListIterator<QPair<MyMoneyTransaction, MyMoneySplit> > it_ts(transactions);
        while (it_ts.hasNext()) {
            const QPair<MyMoneyTransaction, MyMoneySplit> &transactionSplit = it_ts.next();
            QListIterator<MyMoneyMoney> itSum(sumList);
            QList<MyMoneyMoney> tempList;
            while (itSum.hasNext()) {
                const MyMoneyMoney &sum = itSum.next();
                QList<QPair<QString, QString> > splitIds;
                splitIds << qMakePair<QString, QString>(transactionSplit.first.id(), transactionSplit.second.id());
                if (sumToComponentsMap.contains(sum)) {
                    if (sumToComponentsMap.value(sum).contains(qMakePair<QString, QString>(transactionSplit.first.id(), transactionSplit.second.id()))) {
                        continue;
                    }
                    splitIds.append(sumToComponentsMap.value(sum));
                }
                tempList << transactionSplit.second.shares() + sum;
                sumToComponentsMap[transactionSplit.second.shares() + sum] = splitIds;
                int size = sumToComponentsMap.size();
                if (size % PROGRESSBAR_STEPS == 0) {
                    q->slotStatusProgress(size, 0);
                }
                if (size > NR_OF_STEPS_LIMIT) {
                    return result; // it's taking too much resources abort the algorithm
                }
            }
            QList<MyMoneyMoney> unionList;
            unionList.append(tempList);
            unionList.append(sumList);
            qSort(unionList);
            sumList.clear();
            MyMoneyMoney smallestSumFromUnion = unionList.first();
            sumList.append(smallestSumFromUnion);
            QListIterator<MyMoneyMoney> itUnion(unionList);
            while (itUnion.hasNext()) {
                MyMoneyMoney sumFromUnion = itUnion.next();
                if (smallestSumFromUnion < MyMoneyMoney(1 - precision / transactions.size())*sumFromUnion) {
                    smallestSumFromUnion = sumFromUnion;
                    sumList.append(sumFromUnion);
                }
            }
        }

        q->slotStatusProgress(NR_OF_STEPS_LIMIT / PROGRESSBAR_STEPS, 0);
        if (sumToComponentsMap.contains(amount)) {
            QListIterator<QPair<MyMoneyTransaction, MyMoneySplit> > itTransactionSplit(transactions);
            while (itTransactionSplit.hasNext()) {
                const QPair<MyMoneyTransaction, MyMoneySplit> &transactionSplit = itTransactionSplit.next();
                const QList<QPair<QString, QString> > &splitIds = sumToComponentsMap.value(amount);
                if (splitIds.contains(qMakePair<QString, QString>(transactionSplit.first.id(), transactionSplit.second.id()))) {
                    result.append(transactionSplit);
                }
            }
        }

#ifdef KMM_DEBUG
        qDebug("For the amount %s a number of %d possible sums where computed from the set of %d transactions: ",
               qPrintable(MyMoneyUtils::formatMoney(amount, security)), sumToComponentsMap.size(), transactions.size());
#endif

        return result;
    }

    KGlobalLedgerView   *q_ptr;
    MousePressFilter    *m_mousePressFilter;
    KMyMoneyRegister::RegisterSearchLineWidget* m_registerSearchLine;
//  QString              m_reconciliationAccount;
    QDate                m_reconciliationDate;
    MyMoneyMoney         m_endingBalance;
    int                  m_precision;
    bool                 m_recursion;
    bool                 m_showDetails;
    eWidgets::eRegister::Action m_action;

    // models
    AccountNamesFilterProxyModel *m_filterProxyModel;

    // widgets
    KMyMoneyAccountCombo* m_accountComboBox;

    MyMoneyMoney         m_totalBalance;
    bool                 m_balanceIsApproximated;
    // frames
    QFrame*                       m_toolbarFrame;
    QFrame*                       m_registerFrame;
    QFrame*                       m_buttonFrame;
    QFrame*                       m_formFrame;
    QFrame*                       m_summaryFrame;

    // widgets
    KMyMoneyRegister::Register*   m_register;
    KToolBar*                     m_buttonbar;

    /**
      * This member holds the currently selected account
      */
    MyMoneyAccount m_currentAccount;
    QString m_lastSelectedAccountID;

    MyMoneyAccount m_reconciliationAccount;

    /**
      * This member holds the transaction list
      */
    QList<QPair<MyMoneyTransaction, MyMoneySplit> >  m_transactionList;

    QLabel*                         m_leftSummaryLabel;
    QLabel*                         m_centerSummaryLabel;
    QLabel*                         m_rightSummaryLabel;

    KMyMoneyTransactionForm::TransactionForm* m_form;

    /**
      * This member holds the load state of page
      */
    bool                            m_needLoad;

    bool                            m_newAccountLoaded;
    bool                            m_inEditMode;

    QWidgetList                     m_tabOrderWidgets;
    QPoint                          m_tooltipPosn;
    KMyMoneyRegister::SelectedTransactions m_selectedTransactions;
    /**
      * This member keeps the date that was used as the last posting date.
      * It will be updated whenever the user modifies the post date
      * and is used to preset the posting date when new transactions are created.
      * This member is initialised to the current date when the program is started.
      */
    static QDate         m_lastPostDate;
    // pointer to the current transaction editor
    QPointer<TransactionEditor> m_transactionEditor;

    // id's that need to be remembered
    QString  m_accountGoto, m_payeeGoto;
    QString  m_lastPayeeEnteredId;
    QScopedPointer<KBalanceWarning> m_balanceWarning;
    KMyMoneyAccountSelector* m_moveToAccountSelector;

    // Reconciliation dialog
    KEndingBalanceDlg*    m_endingBalanceDlg;
    KFindTransactionDlg*  m_searchDlg;
};

#endif
