/*
    SPDX-FileCopyrightText: 2019-2020 Thomas Baumgart <tbaumgart@kde.org>
    SPDX-License-Identifier: GPL-2.0-or-later
*/

#include "journalmodel.h"

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

#include <QDebug>
#include <QString>
#include <QDate>
#include <QSize>

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

#include <KLocalizedString>

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

#include "mymoneyfile.h"
#include "accountsmodel.h"
#include "costcentermodel.h"
#include "payeesmodel.h"
#include "securitiesmodel.h"
#include "mymoneytransaction.h"
#include "mymoneysplit.h"
#include "mymoneymoney.h"
#include "mymoneytransactionfilter.h"
#include "mymoneyutils.h"

struct JournalModel::Private
{
    typedef enum {
        Interest,
        Fees,
    } category_t;

    Private(JournalModel* qq)
        : q(qq)
        , newTransactionModel(nullptr)
        , headerData(QHash<Column, QString> ({
        { Number, i18nc("Cheque Number", "No.") },
        { Date, i18n("Date") },
        { Account, i18n("Account") },
        { Security, i18n("Security") },
        { CostCenter, i18n("CC") },
        { Detail, i18n("Detail") },
        { Reconciliation, i18n("C") },
        { Payment, i18nc("Payment made from account", "Payment") },
        { Deposit, i18nc("Deposit into account", "Deposit") },
        { Quantity, i18n("Quantity") },
        { Price, i18n("Price") },
        { Amount, i18n("Amount") },
        { Value, i18n("Value") },
        { Balance, i18n("Balance") },
    }))
    {
    }

    QString reconciliationStateShort(eMyMoney::Split::State reconcileState) const
    {
        switch(reconcileState) {
        case eMyMoney::Split::State::NotReconciled:
        default:
            break;
        case eMyMoney::Split::State::Cleared:
            return i18nc("Reconciliation flag C", "C");
        case eMyMoney::Split::State::Reconciled:
            return i18nc("Reconciliation flag R", "R");
        case eMyMoney::Split::State::Frozen:
            return i18nc("Reconciliation flag F", "F");
        }
        return QString();
    }

    QString reconciliationStateLong(eMyMoney::Split::State reconcileState) const
    {
        switch(reconcileState) {
        case eMyMoney::Split::State::NotReconciled:
        default:
            return i18nc("Reconciliation flag empty", "Not reconciled");
        case eMyMoney::Split::State::Cleared:
            return i18nc("Reconciliation flag C", "Cleared");
        case eMyMoney::Split::State::Reconciled:
            return i18nc("Reconciliation flag R", "Reconciled");
        case eMyMoney::Split::State::Frozen:
            return i18nc("Reconciliation flag F", "Frozen");
        }
        return QString();
    }

    QString investmentActivity(const JournalEntry& journalEntry) const
    {
        switch (journalEntry.split().investmentTransactionType()) {
        case eMyMoney::Split::InvestmentTransactionType::AddShares:
            return i18nc("Add securities/shares/bonds", "Add shares");
        case eMyMoney::Split::InvestmentTransactionType::RemoveShares:
            return i18nc("Remove securities/shares/bonds", "Remove shares");
        case eMyMoney::Split::InvestmentTransactionType::BuyShares:
            return i18nc("Buy securities/shares/bonds", "Buy shares");
        case eMyMoney::Split::InvestmentTransactionType::SellShares:
            return i18nc("Sell securities/shares/bonds", "Sell shares");
        case eMyMoney::Split::InvestmentTransactionType::Dividend:
            return i18n("Dividend");
        case eMyMoney::Split::InvestmentTransactionType::ReinvestDividend:
            return i18n("Reinvest Dividend");
        case eMyMoney::Split::InvestmentTransactionType::Yield:
            return i18n("Yield");
        case eMyMoney::Split::InvestmentTransactionType::SplitShares:
            return i18nc("Split securities/shares/bonds", "Split shares");
        case eMyMoney::Split::InvestmentTransactionType::InterestIncome:
            return i18n("Interest Income");
        default:
            return i18nc("Unknown investment activity", "Unknown");
        }
    }

    QString counterAccountId(const JournalEntry& journalEntry, const MyMoneyTransaction& transaction)
    {
        if (transaction.splitCount() == 2) {
            const auto& splitId = journalEntry.split().id();
            for (const auto& sp : transaction.splits()) {
                if(splitId != sp.id()) {
                    return sp.accountId();
                }
            }
        }
        return {};
    }

    QString counterAccount(const QModelIndex& index, const JournalEntry& journalEntry, const MyMoneyTransaction& transaction) const
    {
        // A transaction can have more than 2 splits ...
        const int rows = transaction.splitCount();
        if(rows > 2) {
            // find the first entry of the transaction
            QModelIndex idx = index;
            int row = index.row() - 1;
            for (; row >= 0; --row) {
                idx = q->index(row, 0);
                if (idx.data(eMyMoney::Model::JournalTransactionIdRole).toString() != transaction.id()) {
                    idx = q->index(row++, 0);
                    break;
                }
            }

            QString txt, sep;
            const auto endRow = row + rows;
            for (; row < endRow; ++row) {
                idx = q->index(row, index.column());
                if (idx != index) {
                    const auto accountId = idx.data(eMyMoney::Model::SplitAccountIdRole).toString();
                    idx = MyMoneyFile::instance()->accountsModel()->indexById(accountId);
                    txt += sep + idx.data(eMyMoney::Model::AccountNameRole).toString();
                    sep = QStringLiteral(", ");
                }
            }
            return txt;

            // ... exactly two splits ...
        } else if(rows == 2) {
            const auto& splitId = journalEntry.split().id();
            for (const auto& sp : transaction.splits()) {
                if(splitId != sp.id()) {
                    return MyMoneyFile::instance()->accountsModel()->accountIdToHierarchicalName(sp.accountId());
                }
            }

            // ... or a single split
        } else if(!journalEntry.split().shares().isZero()) {
            return i18n("*** UNASSIGNED ***");
        }
        return QString();
    }

    QString investmentBrokerageAccount(const JournalEntry& journalEntry) const
    {
        const auto file = MyMoneyFile::instance();
        if (file->isInvestmentTransaction(journalEntry.transaction())) {
            for (const auto& split : journalEntry.transaction().splits()) {
                const auto acc = file->account(split.accountId());
                if (acc.isAssetLiability() && !acc.isInvest() && (acc.accountType() != eMyMoney::Account::Type::Investment)) {
                    return acc.name();
                }
            }
        }
        return {};
    }

    QString interestOrFeeCategory(const JournalEntry& journalEntry, category_t type) const
    {
        const bool fees = (type == Fees);
        QString rc;
        const auto file = MyMoneyFile::instance();
        if (file->isInvestmentTransaction(journalEntry.transaction())) {
            for (const auto& split : journalEntry.transaction().splits()) {
                const auto acc = file->account(split.accountId());
                if (acc.isIncomeExpense()) {
                    if (split.shares().isNegative() ^ fees) {
                        if (rc.isEmpty()) {
                            rc = MyMoneyFile::instance()->accountsModel()->accountIdToHierarchicalName(split.accountId());
                        } else {
                            return fees ? i18n("Multiple fee categories") : i18n("Multiple interest categories");
                        }
                    }
                }
            }
        }
        return rc;
    }

    MyMoneyMoney interestOrFeeValue(const JournalEntry& journalEntry, category_t type) const
    {
        const bool fees = (type == Fees);
        MyMoneyMoney value;
        const auto file = MyMoneyFile::instance();
        if (file->isInvestmentTransaction(journalEntry.transaction())) {
            for (const auto& split : journalEntry.transaction().splits()) {
                const auto acc = file->account(split.accountId());
                if (acc.isIncomeExpense()) {
                    if (split.shares().isNegative() ^ fees) {
                        value += split.value();
                    }
                }
            }
        }
        return value;
    }

    bool haveInterestOrFeeSplit(const JournalEntry& journalEntry, category_t type) const
    {
        const auto file = MyMoneyFile::instance();
        if (file->isInvestmentTransaction(journalEntry.transaction())) {
            for (const auto& split : journalEntry.transaction().splits()) {
                const auto acc = file->account(split.accountId());
                if (acc.isIncomeExpense()) {
                    if (split.shares().isNegative() && (type == Interest)) {
                        return true;
                    }
                    else if (split.shares().isPositive() && (type == Fees)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    MyMoneySecurity security(const JournalEntry& journalEntry) const
    {
        const auto file = MyMoneyFile::instance();
        const auto acc = file->accountsModel()->itemById(journalEntry.split().accountId());
        return file->securitiesModel()->itemById(acc.currencyId());
    }

    void removeIdKeyMapping(const QString& id)
    {
        transactionIdKeyMap.remove(id);
    }

    void addIdKeyMapping(const QString& id, const QString& key)
    {
        transactionIdKeyMap[id] = key;
    }

    QString mapIdToKey(const QString& id) const
    {
        return transactionIdKeyMap.value(id);
    }

    void loadAccountCache()
    {
        accountCache.clear();
        const auto accountList = MyMoneyFile::instance()->accountsModel()->itemList();
        foreach(const auto& acc, accountList) {
            accountCache[acc.id()] = acc;
        }
    }

    void startBalanceCacheOperation()
    {
        balanceChangedSet.clear();
        fullBalanceRecalc.clear();
    }

    void removeTransactionFromBalance(int startRow, int rows)
    {
        for (int row = 0; row < rows; ++row)  {
            const auto journalEntry = static_cast<TreeItem<JournalEntry>*>(q->index(startRow, 0).internalPointer())->constDataRef();
            balanceChangedSet.insert(journalEntry.split().accountId());
            if (Q_UNLIKELY(journalEntry.transaction().isStockSplit())) {
                fullBalanceRecalc.insert(journalEntry.split().accountId());
            } else {
                balanceCache[journalEntry.split().accountId()] -= journalEntry.split().shares();
            }
            ++startRow;
        }
    }

    void addTransactionToBalance(int startRow, int rows)
    {
        for (int row = 0; row < rows; ++row)  {
            const auto journalEntry = static_cast<TreeItem<JournalEntry>*>(q->index(startRow, 0).internalPointer())->constDataRef();
            balanceChangedSet.insert(journalEntry.split().accountId());
            if (Q_UNLIKELY(journalEntry.transaction().isStockSplit())) {
                fullBalanceRecalc.insert(journalEntry.split().accountId());
            } else {
                balanceCache[journalEntry.split().accountId()] += journalEntry.split().shares();
            }
            ++startRow;
        }
    }

    void finishBalanceCacheOperation()
    {
        if (!fullBalanceRecalc.isEmpty()) {
            const auto journalRows = q->rowCount();
            for (const auto& accountId : qAsConst(fullBalanceRecalc)) {
                balanceCache[accountId] = MyMoneyMoney();
            }

            for (int row = 0; row < journalRows; ++row) {
                const JournalEntry& journalEntry = static_cast<TreeItem<JournalEntry>*>(q->index(row, 0).internalPointer())->constDataRef();
                if (fullBalanceRecalc.contains(journalEntry.split().accountId())) {
                    if (journalEntry.transaction().isStockSplit()) {
                        balanceCache[journalEntry.split().accountId()] *= journalEntry.split().shares();
                    } else {
                        balanceCache[journalEntry.split().accountId()] += journalEntry.split().shares();
                    }
                }
            }
        }

        // inform others about the changes
        QHash<QString, MyMoneyMoney> balances;
        for (const auto& accountId : qAsConst(balanceChangedSet)) {
            balances.insert(accountId, balanceCache.value(accountId));
            emit q->balanceChanged(accountId);
        }
        emit q->balancesChanged(balances);
    }

    QString formatValue(const MyMoneyTransaction& t, const MyMoneySplit& s, const MyMoneyMoney& factor = MyMoneyMoney::ONE)
    {
        auto acc = MyMoneyFile::instance()->accountsModel()->itemById(s.accountId());
        auto value = s.value(t.commodity(), acc.currencyId());
        return (value * factor).formatMoney(acc.fraction());
    }

    QString formatShares(const MyMoneySplit& s)
    {
        auto acc = MyMoneyFile::instance()->accountsModel()->itemById(s.accountId());
        return (s.shares().abs()).formatMoney(acc.fraction());
    }

    JournalModel*                   q;
    JournalModelNewTransaction*     newTransactionModel;
    QMap<QString, QString>          transactionIdKeyMap;
    QHash<Column, QString>          headerData;
    QHash<QString, MyMoneyMoney>    balanceCache;
    QHash<QString, MyMoneyAccount>  accountCache;
    QSet<QString>                   fullBalanceRecalc;
    QSet<QString>                   balanceChangedSet;
};

JournalModelNewTransaction::JournalModelNewTransaction(QObject* parent)
    : JournalModel(parent)
{
    setObjectName(QLatin1String("JournalModelNewTransaction"));
    QMap<QString, MyMoneyTransaction> list;
    MyMoneyTransaction t;
    MyMoneySplit sp;
    sp.setAccountId(fakeId());
    t.addSplit(sp);
    list[QString()] = t;
    JournalModel::load(list);
}

JournalModelNewTransaction::~JournalModelNewTransaction()
{
}

QVariant JournalModelNewTransaction::data(const QModelIndex& idx, int role) const
{
    if (!idx.isValid())
        return QVariant();
    if (idx.row() < 0 || idx.row() >= rowCount(idx.parent()))
        return QVariant();

    // never show any data for the empty transaction
    if ((role == Qt::DisplayRole) || (role == Qt::EditRole))
        return QVariant();

    return JournalModel::data(idx, role);
}





JournalModel::JournalModel(QObject* parent, QUndoStack* undoStack)
    : MyMoneyModel<JournalEntry>(parent, QStringLiteral("T"), JournalModel::ID_SIZE, undoStack)
    , d(new Private(this))
{
    setObjectName(QLatin1String("JournalModel"));
}

JournalModel::JournalModel(const QString& idLeadin, QObject* parent, QUndoStack* undoStack)
    : MyMoneyModel<JournalEntry>(parent, idLeadin, JournalModel::ID_SIZE, undoStack)
    , d(new Private(this))
{
    setObjectName(QLatin1String("JournalModel"));
}

JournalModel::~JournalModel()
{
}

JournalModelNewTransaction * JournalModel::newTransaction()
{
    if (d->newTransactionModel == nullptr) {
        d->newTransactionModel = new JournalModelNewTransaction(this);
    }
    return d->newTransactionModel;
}

int JournalModel::columnCount(const QModelIndex& parent) const
{
    Q_UNUSED(parent);
    Q_ASSERT(d->headerData.count() == MaxColumns);

    return MaxColumns;
}

QVariant JournalModel::headerData(int section, Qt::Orientation orientation, int role) const
{
    if (orientation == Qt::Horizontal) {
        switch (role) {
        case Qt::DisplayRole:
            return d->headerData.value(static_cast<Column>(section));

        case Qt::SizeHintRole:
            return QSize(20, 20);
        }
        return {};
    }
    if (orientation == Qt::Vertical && role == Qt::SizeHintRole) {
        return QSize(10, 10);
    }
    return QAbstractItemModel::headerData(section, orientation, role);
}

Qt::ItemFlags JournalModel::flags(const QModelIndex& index) const
{
    Q_UNUSED(index);
    return (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable);
}

QVariant JournalModel::data(const QModelIndex& idx, int role) const
{
    if (!idx.isValid())
        return QVariant();
    if (idx.row() < 0 || idx.row() >= rowCount(idx.parent()))
        return QVariant();

    const JournalEntry& journalEntry = static_cast<TreeItem<JournalEntry>*>(idx.internalPointer())->constDataRef();
    if (journalEntry.transactionPtr() == nullptr) {
        return QVariant();
    }

    const MyMoneyTransaction& transaction = journalEntry.transaction();
    const MyMoneySplit& split = journalEntry.split();

    switch(role) {
    case Qt::DisplayRole:
    case Qt::EditRole:
        switch(idx.column()) {
        case Number:
            return journalEntry.split().number();

        case Date:
            return MyMoneyUtils::formatDate(transaction.postDate());

        case Account:
            return MyMoneyFile::instance()->accountsModel()->itemById(journalEntry.split().accountId()).name();

        case Security:
            return d->security(journalEntry).name();
            break;

        case CostCenter:
#if 0
            /// @todo finish implementation
            // in case the own split does not have a costcenter, but the counter split does
            // we use it nevertheless
            if(m_costCenterId.isEmpty())
                m_costCenterId = split.costCenterId();
#endif
            break;

        case Detail:
            return d->counterAccount(idx, journalEntry, transaction);

        case Reconciliation:
            return d->reconciliationStateShort(journalEntry.split().reconcileFlag());
            break;

        case Payment:
            if (journalEntry.split().value().isNegative()) {
                return d->formatValue(transaction, journalEntry.split(), MyMoneyMoney::MINUS_ONE);
            }
            break;

        case Deposit:
            if (!journalEntry.split().value().isNegative()) {
                return d->formatValue(transaction, journalEntry.split(), MyMoneyMoney::ONE);
            }
            break;

        case Quantity:
            switch(journalEntry.split().investmentTransactionType()) {
            case eMyMoney::Split::InvestmentTransactionType::Dividend:
            case eMyMoney::Split::InvestmentTransactionType::Yield:
            case eMyMoney::Split::InvestmentTransactionType::InterestIncome:
                break;
            case eMyMoney::Split::InvestmentTransactionType::SplitShares:
                return QString("1 / %1").arg(split.shares().abs().formatMoney(QString(), -1));
                break;
            default:
                return split.shares().abs().formatMoney(QString(), MyMoneyMoney::denomToPrec(d->security(journalEntry).smallestAccountFraction()));
            }
            break;

        case Price:
            switch(journalEntry.split().investmentTransactionType()) {
            case eMyMoney::Split::InvestmentTransactionType::BuyShares:
            case eMyMoney::Split::InvestmentTransactionType::SellShares:
            case eMyMoney::Split::InvestmentTransactionType::ReinvestDividend:
                if (!split.shares().isZero()) {
                    return split.price().formatMoney(MyMoneyFile::instance()->currency(transaction.commodity()).tradingSymbol(), d->security(journalEntry).pricePrecision());

                }
                break;
            default:
                break;
            }
            break;

        case Amount:
        case Value:
        {
            MyMoneySplit assetAccountSplit;
            QList<MyMoneySplit> feeSplits, interestSplits;
            MyMoneySecurity security, currency;
            MyMoneyMoney amount;
            eMyMoney::Split::InvestmentTransactionType transactionType;
            MyMoneyUtils::dissectTransaction(transaction, split, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType);
            switch(transactionType) {
            case eMyMoney::Split::InvestmentTransactionType::BuyShares:
            case eMyMoney::Split::InvestmentTransactionType::SellShares:
            case eMyMoney::Split::InvestmentTransactionType::Dividend:
            case eMyMoney::Split::InvestmentTransactionType::Yield:
            case eMyMoney::Split::InvestmentTransactionType::InterestIncome:
                return MyMoneyUtils::formatMoney(assetAccountSplit.value().abs(), currency);

            case eMyMoney::Split::InvestmentTransactionType::ReinvestDividend:
                for(const auto& sp : qAsConst(interestSplits)) {
                    amount += sp.value();
                }
                return  MyMoneyUtils::formatMoney(-amount, currency);

            default:
                break;
            }
        }
        break;

        case Balance:
        {
            const auto sp = journalEntry.split();
            auto acc = MyMoneyFile::instance()->accountsModel()->itemById(sp.accountId());
            return journalEntry.balance().formatMoney(acc.fraction());
        }
        break;
        }
        break;

    case Qt::TextAlignmentRole:
        switch( idx.column()) {
        case Quantity:
        case Price:
        case Amount:
        case Payment:
        case Deposit:
        case Balance:
        case Value:
            return QVariant(Qt::AlignRight | Qt::AlignTop);

        case Reconciliation:
            return QVariant(Qt::AlignHCenter | Qt::AlignTop);

        default:
            break;
        }
        return QVariant(Qt::AlignLeft | Qt::AlignTop);

    case eMyMoney::Model::IdRole:
        return journalEntry.id();

    case eMyMoney::Model::SplitAccountIdRole:
        return journalEntry.split().accountId();

    case eMyMoney::Model::SplitReconcileFlagRole:
        return QVariant::fromValue<eMyMoney::Split::State>(journalEntry.split().reconcileFlag());

    case eMyMoney::Model::SplitReconcileDateRole:
        return journalEntry.split().reconcileDate();

    case eMyMoney::Model::SplitActionRole:
        return journalEntry.split().action();

    case eMyMoney::Model::JournalSplitIdRole:
        return journalEntry.split().id();

    case eMyMoney::Model::JournalSplitAccountIdRole:
        return journalEntry.split().accountId();

    case eMyMoney::Model::JournalTransactionIdRole:
        return journalEntry.transaction().id();

    case eMyMoney::Model::TransactionSplitCountRole:
        return journalEntry.transaction().splitCount();

    case eMyMoney::Model::TransactionIsTransferRole:
        if (journalEntry.transaction().splitCount() == 2) {
            for (const auto& sp : journalEntry.transaction().splits()) {
                const auto acc = MyMoneyFile::instance()->accountsModel()->itemById(sp.accountId());
                if (acc.isIncomeExpense()) {
                    return false;
                }
            }
        }
        return true;

    case eMyMoney::Model::TransactionIsInvestmentRole:
        return MyMoneyFile::instance()->isInvestmentTransaction(journalEntry.transaction());

    case eMyMoney::Model::TransactionInvestementType:
        return QVariant::fromValue<eMyMoney::Split::InvestmentTransactionType>(journalEntry.split().investmentTransactionType());

    case eMyMoney::Model::TransactionBrokerageAccountRole:
        return d->investmentBrokerageAccount(journalEntry);

    case eMyMoney::Model::TransactionInterestCategoryRole:
        return d->interestOrFeeCategory(journalEntry, Private::Interest);

    case eMyMoney::Model::TransactionFeesCategoryRole:
        return d->interestOrFeeCategory(journalEntry, Private::Fees);

    case eMyMoney::Model::TransactionPostDateRole:
        return transaction.postDate();

    case eMyMoney::Model::TransactionIsStockSplitRole:
        return transaction.isStockSplit();

    case eMyMoney::Model::TransactionErroneousRole:
        return !transaction.splitSum().isZero();

    case eMyMoney::Model::TransactionImportedRole:
        return transaction.isImported();

    case eMyMoney::Model::TransactionInvestmentAccountIdRole:
        if (MyMoneyFile::instance()->isInvestmentTransaction(journalEntry.transaction())) {
            QString accountId;
            for (const auto& sp : journalEntry.transaction().splits()) {
                const auto acc = MyMoneyFile::instance()->accountsModel()->itemById(sp.accountId());
                if (acc.accountType() == eMyMoney::Account::Type::Investment) {
                    return acc.id();
                }
                if (acc.isInvest()) {
                    accountId = acc.parentAccountId();
                }
            }
            return accountId;
        }
        break;

    case eMyMoney::Model::TransactionInterestValueRole:
        return QVariant::fromValue<MyMoneyMoney>(d->interestOrFeeValue(journalEntry, Private::Interest));

    case eMyMoney::Model::TransactionFeesValueRole:
        return QVariant::fromValue<MyMoneyMoney>(d->interestOrFeeValue(journalEntry, Private::Fees));

    case eMyMoney::Model::TransactionInterestSplitPresentRole:
        return d->haveInterestOrFeeSplit(journalEntry, Private::Interest);

    case eMyMoney::Model::TransactionFeeSplitPresentRole:
        return d->haveInterestOrFeeSplit(journalEntry, Private::Fees);

    case eMyMoney::Model::TransactionCommodityRole:
        return transaction.commodity();

    case eMyMoney::Model::SplitSharesSuffixRole:
        // figure out if it is a debit or credit split. s.a. https://en.wikipedia.org/wiki/Debits_and_credits#Aspects_of_transactions
        if(journalEntry.split().shares().isNegative()) {
            return i18nc("Credit suffix", "Cr.");
        }
        return i18nc("Debit suffix", "Dr.");

    case eMyMoney::Model::SplitSharesRole:
    {
        QVariant rc;
        rc.setValue(journalEntry.split().shares());
        return rc;
    }

    case eMyMoney::Model::SplitFormattedValueRole:
        return d->formatValue(transaction, journalEntry.split(), MyMoneyMoney::ONE);

    case eMyMoney::Model::SplitFormattedSharesRole:
        return d->formatShares(split);

    case eMyMoney::Model::SplitValueRole:
    {
        QVariant rc;
        rc.setValue(journalEntry.split().value());
        return rc;
    }

    case eMyMoney::Model::SplitPriceRole:
    {
        QVariant rc;
        rc.setValue(journalEntry.split().price());
        return rc;
    }

    case eMyMoney::Model::SplitPayeeIdRole:
        if (journalEntry.split().payeeId().isEmpty()) {
            // not sure if we want to replace it with the payeeId
            // of another split. Anyway, here would be the spot to do it
#if 0
            const MyMoneySplit split = journalEntry.split();
            foreach (const auto sp, transaction.splits()) {
                if(split.id() != sp.id()) {
                    if (!split.payeeId().isEmpty())

                    }
            }
#endif
            return QVariant();
        }
        return journalEntry.split().payeeId();

    case eMyMoney::Model::SplitTagIdRole:
        return QVariant::fromValue<QStringList>(journalEntry.split().tagIdList());

    case eMyMoney::Model::SplitSingleLineMemoRole:
    case eMyMoney::Model::SplitMemoRole:
    {
        QString rc(journalEntry.split().memo());
        if(role == eMyMoney::Model::SplitSingleLineMemoRole) {
            // remove empty lines
            rc.replace("\n\n", "\n");
            // replace '\n' with ", "
            rc.replace('\n', ", ");
        }
        return rc;
    }

    case eMyMoney::Model::SplitPayeeRole:
        return MyMoneyFile::instance()->payeesModel()->itemById(journalEntry.split().payeeId()).name();

    case eMyMoney::Model::SplitMatchedRole:
        return journalEntry.split().isMatched();

    case eMyMoney::Model::TransactionCounterAccountRole:
        return d->counterAccount(idx, journalEntry, transaction);

    case eMyMoney::Model::TransactionCounterAccountIdRole:
        return d->counterAccountId(journalEntry, transaction);

    case eMyMoney::Model::TransactionScheduleRole:
        return false;

    case eMyMoney::Model::JournalSplitPaymentRole:
        if (journalEntry.split().value().isNegative()) {
            return d->formatValue(transaction, journalEntry.split(), MyMoneyMoney::MINUS_ONE);
        }
        break;

    case eMyMoney::Model::JournalSplitDepositRole:
        if (!journalEntry.split().value().isNegative()) {
            return d->formatValue(transaction, journalEntry.split(), MyMoneyMoney::ONE);
        }
        break;

    case eMyMoney::Model::SplitSharesFormattedRole:
        return d->formatShares(journalEntry.split());

    case eMyMoney::Model::SplitNumberRole:
        return journalEntry.split().number();

    case eMyMoney::Model::SplitActivityRole:
        return d->investmentActivity(journalEntry);

    default:
        if (role >= Qt::UserRole)
            qDebug() << "JournalModel::data(), role" << role << "offset" << role-Qt::UserRole << "not implemented";
        break;
    }
    return QVariant();
}

bool JournalModel::setData(const QModelIndex& idx, const QVariant& value, int role)
{
    if(!idx.isValid()) {
        return false;
    }
    if (idx.row() < 0 || idx.row() >= rowCount(idx.parent())) {
        return false;
    }

    JournalEntry& journalEntry = static_cast<TreeItem<JournalEntry>*>(idx.internalPointer())->dataRef();
    if (journalEntry.transactionPtr() == nullptr) {
        return false;
    }

    switch(role) {
    case Qt::DisplayRole:
    case Qt::EditRole:
        switch(idx.column()) {
        case Balance:
            journalEntry.setBalance(value.value<MyMoneyMoney>());
            return true;

        default:
            return false;
        }
        break;

    default:
        return false;

    }
    // qDebug() << "setData(" << idx.row() << idx.column() << ")" << value << role;
    return QAbstractItemModel::setData(idx, value, role);
}

void JournalModel::load(const QMap<QString, MyMoneyTransaction>& list)
{
    QElapsedTimer t;

    t.start();
    beginResetModel();
    // first get rid of any existing entries
    clearModelItems();

    // create the number of required items
    int itemCount = 0;
    for (const auto& item : qAsConst(list)) {
        itemCount += item.splitCount();
    }
    insertRows(0, itemCount);

    m_nextId = 0;

    int row = 0;
    QMap<QString, MyMoneyTransaction>::const_iterator it;
    for (it = list.constBegin(); it != list.constEnd(); ++it) {
        const QString& id = (*it).id();
        updateNextObjectId(id);
        d->addIdKeyMapping(id, it.key());
        auto transaction = QSharedPointer<MyMoneyTransaction>(new MyMoneyTransaction(*it));
        for (const auto& split : (*transaction).splits()) {
            const JournalEntry journalEntry(it.key(), transaction, split);
            static_cast<TreeItem<JournalEntry>*>(index(row, 0).internalPointer())->dataRef() = journalEntry;
            ++row;
        }
    }
    endResetModel();

    emit modelLoaded();

    // and don't count loading as a modification
    setDirty(false);

    qDebug() << "Model for" << m_idLeadin << "loaded with" << rowCount() << "items in" << t.elapsed() << "ms";
}

void JournalModel::unload()
{
    d->balanceCache.clear();
    d->accountCache.clear();
    d->transactionIdKeyMap.clear();
    MyMoneyModel::unload();
}

MyMoneyTransaction JournalModel::transactionById(const QString& id) const
{
    const QModelIndex idx = firstIndexById(id);
    if (idx.isValid()) {
        return static_cast<TreeItem<JournalEntry>*>(idx.internalPointer())->constDataRef().transaction();
    }
    return MyMoneyTransaction();
}

QModelIndex JournalModel::firstIndexById(const QString& id) const
{
    const QString key = d->mapIdToKey(id);
    // in case we do not have a key, the transactionId does not exist
    // so no need to search for it
    if (key.isEmpty()) {
        return QModelIndex();
    }

    return MyMoneyModelBase::lowerBound(key);
}

QModelIndexList JournalModel::indexesByTransactionId(const QString& id) const
{
    QModelIndexList indexes;
    QModelIndex idx = firstIndexById(id);
    while (idx.isValid() && (idx.data(eMyMoney::Model::JournalTransactionIdRole).toString() == id)) {
        indexes.append(idx);
        idx = index(idx.row()+1, 0);
    }
    return indexes;
}

QString JournalModel::keyForDate(const QDate& date) const
{
    return MyMoneyTransaction::uniqueSortKey(date, QString());
}

void JournalModel::addTransaction(MyMoneyTransaction& item)
{
    item = MyMoneyTransaction(nextId(), item);
    auto transaction = QSharedPointer<MyMoneyTransaction>(new MyMoneyTransaction(item));
    JournalEntry entry(QString(), transaction, MyMoneySplit());

    m_undoStack->push(new UndoCommand(this, JournalEntry(), entry));
}

void JournalModel::doAddItem(const JournalEntry& item, const QModelIndex& parentIdx)
{
    Q_UNUSED(parentIdx);
    auto transaction = item.sharedtransactionPtr();
    QString key = (*transaction).uniqueSortKey();

    // add mapping
    d->addIdKeyMapping((*transaction).id(), key);

    const auto idx = MyMoneyModelBase::lowerBound(key);
    auto startRow = idx.row();
    if (!idx.isValid()) {
        startRow = rowCount();
    }

    const int rows = (*transaction).splitCount();

    // insert the items into the model
    insertRows(startRow, rows);
    const QModelIndex startIdx = index(startRow, 0);
    const QModelIndex endIdx = index(startRow+rows-1, columnCount()-1);

    d->startBalanceCacheOperation();

    const auto originalStartRow = startRow;
    foreach (const auto split, (*transaction).splits()) {
        JournalEntry journalEntry(key, transaction, split);
        static_cast<TreeItem<JournalEntry>*>(index(startRow, 0).internalPointer())->dataRef() = journalEntry;
        ++startRow;
    }

    // add the splits to the balance cache
    d->addTransactionToBalance(originalStartRow, rows);

    emit dataChanged(startIdx, endIdx);

    d->finishBalanceCacheOperation();
    setDirty();
}

void JournalModel::removeTransaction(const MyMoneyTransaction& item)
{
    const auto idx = firstIndexById(item.id());
    if (idx.isValid()) {
        const auto currentItem = static_cast<TreeItem<JournalEntry>*>(idx.internalPointer())->constDataRef();
        m_undoStack->push(new UndoCommand(this, currentItem, JournalEntry()));
    }
}

void JournalModel::doRemoveItem(const JournalEntry& before)
{

    const auto& transaction = before.transaction();
    const auto idx = firstIndexById(transaction.id());
    const auto rows = transaction.splitCount();
    d->startBalanceCacheOperation();
    d->removeTransactionFromBalance(idx.row(), rows);

    removeRows(idx.row(), rows);
    d->removeIdKeyMapping(transaction.id());

    d->finishBalanceCacheOperation();
    setDirty();
}

void JournalModel::modifyTransaction(const MyMoneyTransaction& newTransaction)
{
    const auto idx = firstIndexById(newTransaction.id());
    if (idx.isValid()) {
        auto transaction = QSharedPointer<MyMoneyTransaction>(new MyMoneyTransaction(newTransaction));
        JournalEntry entry(QString(), transaction, MyMoneySplit());

        const auto currentItem = static_cast<TreeItem<JournalEntry>*>(idx.internalPointer())->constDataRef();
        m_undoStack->push(new UndoCommand(this, currentItem, entry));
    }
}

void JournalModel::doModifyItem(const JournalEntry& before, const JournalEntry& after)
{
    Q_UNUSED(before)
    auto srcIdx = firstIndexById(after.transaction().id());

    if (!srcIdx.isValid())
        return;

    // we keep a copy of the original transaction
    // (we don't believe the caller except for the id)
    const auto newTransaction = after.transaction();
    const auto newSplitCount = static_cast<int>(newTransaction.splitCount());
    const auto newKey = newTransaction.uniqueSortKey();

    const auto oldTransaction = static_cast<TreeItem<JournalEntry>*>(srcIdx.internalPointer())->constDataRef().transaction();
    const auto oldSplitCount = static_cast<int>(oldTransaction.splitCount());
    const auto oldKey = oldTransaction.uniqueSortKey();

    d->startBalanceCacheOperation();
    d->removeTransactionFromBalance(srcIdx.row(), oldSplitCount);

    // we have to deal with several cases here. The first differentiation
    // is the unique key. It remains the same as long as the postDate()
    // of the two transactions is identical. In case the postDate changed, we
    // need to move the transaction to a new spot in the model. This move
    // is done last so that we have the complete new data already in the model.
    // Besides the post date we have to differentiate between three other cases:
    //
    //   a) number of splits increases
    //   b) number of splits decreases
    //   c) number of splits remains the same
    //
    // In a first step we take care of cases a) and b) by inserting
    // or removing rows.
    // In the next step we simply assign new JournalEntry items to
    // the model for all splits.
    // And as last act we move the new items around

    // Step 1
    if (newSplitCount > oldSplitCount) {
        insertRows(srcIdx.row() + oldSplitCount, newSplitCount - oldSplitCount);

    } else if (newSplitCount < oldSplitCount) {
        removeRows(srcIdx.row() + newSplitCount, oldSplitCount - newSplitCount);
    }

    // Step 2
    auto transaction = after.sharedtransactionPtr();

    // use the oldKey for now to keep sorting in a correct state
    int row = srcIdx.row();
    foreach (const auto split, newTransaction.splits()) {
        JournalEntry journalEntry(oldKey, transaction, split);
        static_cast<TreeItem<JournalEntry>*>(index(row, 0).internalPointer())->dataRef() = journalEntry;
        ++row;
    }

    // let the world know that things have changed
    QModelIndex endIdx = index(row-1, columnCount()-1);
    emit dataChanged(srcIdx, endIdx);

    // Step 3
    if (newTransaction.postDate() != oldTransaction.postDate()) {
        const auto destIdx = MyMoneyModelBase::lowerBound(newKey);
        auto destRow = destIdx.row();
        if (!destIdx.isValid()) {
            destRow = rowCount();
        }
        // we can skip moving if there is no transaction between
        // the current location and the new one
        if (destRow != (srcIdx.row() + newSplitCount)) {
            beginMoveRows(QModelIndex(), srcIdx.row(), srcIdx.row() + newSplitCount - 1, QModelIndex(), destRow);

            d->removeIdKeyMapping(oldTransaction.id());

            // take the items out of their old location
            // and update the key (kept in their m_id)
            QVector<TreeItem<JournalEntry>*> entries;
            const int srcRow = srcIdx.row();
            for (int rows = newSplitCount; rows > 0; --rows) {
                auto journalEntry = m_rootItem->takeChild(srcRow);
                journalEntry->dataRef().m_id = newKey;
                entries.append(journalEntry);
            }
            // check if the destination row must be adjusted
            // since we removed the splits already
            if (srcIdx.row() < destRow)
                destRow -= newSplitCount;

            // insert the items at the new location
            m_rootItem->insertChildren(destRow, entries);

            d->addIdKeyMapping(oldTransaction.id(), newKey);

            endMoveRows();

            int firstRow, lastRow;
            if (srcRow < destRow) {
                firstRow = srcRow;
                lastRow = destRow + newSplitCount - 1;
            } else {
                firstRow = destRow;
                lastRow = srcRow + newSplitCount - 1;
            }
            emit dataChanged(index(firstRow, 0), index(lastRow, columnCount()-1));

            // update the index of the transaction
            srcIdx = index(destRow, 0);
        }
    }

    d->addTransactionToBalance(srcIdx.row(), newTransaction.splitCount());

    d->finishBalanceCacheOperation();
    setDirty();
}

void JournalModel::transactionList(QList<MyMoneyTransaction>& list, MyMoneyTransactionFilter& filter) const
{
    list.clear();

    const int rows = rowCount();
    for (int row = 0; row < rows;) {
        const auto journalEntry = static_cast<TreeItem<JournalEntry>*>(index(row, 0).internalPointer())->constDataRef();
        const auto cnt = filter.matchingSplitsCount(journalEntry.transaction());
        for (uint i = 0; i < cnt; ++i) {
            list.append(journalEntry.transaction());
        }
        row += journalEntry.transaction().splitCount();
    }
}

void JournalModel::transactionList(QList< QPair<MyMoneyTransaction, MyMoneySplit> >& list, MyMoneyTransactionFilter& filter) const
{
    list.clear();

    const int rows = rowCount();
    QVector<MyMoneySplit> splits;
    for (int row = 0; row < rows; ) {
        const JournalEntry& journalEntry = static_cast<TreeItem<JournalEntry>*>(index(row, 0).internalPointer())->constDataRef();
        splits = filter.matchingSplits(journalEntry.transaction());
        if (!splits.isEmpty()) {
            for (const auto& split : qAsConst(splits)) {
                list.append(qMakePair(journalEntry.transaction(), split));
            }
        }
        row += journalEntry.transaction().splitCount();
    }
}

unsigned int JournalModel::transactionCount(const QString& accountid) const
{
    unsigned int result = 0;

    if (accountid.isEmpty()) {
        result = d->transactionIdKeyMap.count();

    } else {
        const int rows = rowCount();
        for (int row = 0; row < rows; ++row) {
            const JournalEntry& journalEntry = static_cast<TreeItem<JournalEntry>*>(index(row, 0).internalPointer())->constDataRef();
            if (journalEntry.split().accountId() == accountid) {
                ++result;
            }
        }
    }
    return result;
}

void JournalModel::updateBalances()
{
    d->loadAccountCache();

    // calculate the balances
    d->balanceCache.clear();
    const int rows = rowCount();
    qDebug() << "Start calculating balances:" << rows << "splits";
    for (int row = 0; row < rows; ++row) {
        const JournalEntry& journalEntry = static_cast<TreeItem<JournalEntry>*>(index(row, 0).internalPointer())->constDataRef();
        if (journalEntry.transaction().isStockSplit()) {
            d->balanceCache[journalEntry.split().accountId()] *= journalEntry.split().shares();
        } else {
            d->balanceCache[journalEntry.split().accountId()] += journalEntry.split().shares();
        }
    }
    qDebug() << "End calculating balances";

    // and store the results in the accountsModel
    emit balancesChanged(d->balanceCache);
}

MyMoneyMoney JournalModel::balance(const QString& accountId, const QDate& date) const
{
    if (date.isValid()) {
        MyMoneyMoney balance;
        QModelIndex lastIdx = upperBound(MyMoneyTransaction::uniqueSortKey(date, QStringLiteral("x")), 0, rowCount()-1);
        // in case the index is invalid, we search for a data past
        // the end of the journal, so we can simply use the cached
        // balance if it is available. Otherwise, we have to go
        // through the journal
        if (lastIdx.isValid() || !d->balanceCache.contains(accountId)) {
            // in case the entry is in the first half,
            // we start from the beginning and go forward
            auto lastRow = lastIdx.row();

            // if the lastRow is -1 we have an invalid index which means,
            // that the balance cache is not filled and we have to scan
            // all journal entries.
            if (lastRow == -1) {
                lastRow = rowCount();
            }

            // invalid index or first half?
            if (lastIdx.row() < rowCount()/2)  {
                for (int row = 0; row < lastRow; ++row) {
                    const JournalEntry& journalEntry = static_cast<TreeItem<JournalEntry>*>(index(row, 0).internalPointer())->constDataRef();
                    if (journalEntry.split().accountId() == accountId) {
                        if (journalEntry.transaction().isStockSplit()) {
                            balance *= journalEntry.split().shares();
                        } else {
                            balance += journalEntry.split().shares();
                        }
                    }
                }
            } else {
                // in case the entry is in the second half,
                // we start at the end and go backwards
                // This requires the balance cache to always
                // be up-to-date
                balance = d->balanceCache.value(accountId);
                for (int row = rowCount()-1; row >= lastIdx.row(); --row) {
                    const JournalEntry& journalEntry = static_cast<TreeItem<JournalEntry>*>(index(row, 0).internalPointer())->constDataRef();
                    if (journalEntry.split().accountId() == accountId) {
                        if (journalEntry.transaction().isStockSplit()) {
                            balance /= journalEntry.split().shares();
                        } else {
                            balance -= journalEntry.split().shares();
                        }
                    }
                }
            }
            return balance;
        }
    }
    return d->balanceCache.value(accountId);
}

// determine for which type we were created:
//
// a) add item: m_after.id() is filled, m_before.id() is empty
// b) modify item: m_after.id() is filled, m_before.id() is filled
// c) add item: m_after.id() is empty, m_before.id() is filled
JournalModel::Operation JournalModel::undoOperation(const JournalEntry& before, const JournalEntry& after) const
{
    const auto afterIdEmpty = (after.transactionPtr() == nullptr) || (after.transaction().id().isEmpty());
    const auto beforeIdEmpty = (before.transactionPtr() == nullptr) || (before.transaction().id().isEmpty());
    if (!afterIdEmpty && beforeIdEmpty)
        return Add;
    if (!afterIdEmpty && !beforeIdEmpty)
        return Modify;
    if (afterIdEmpty && !beforeIdEmpty)
        return Remove;
    return Invalid;
}

QString JournalModel::fakeId() const
{
    return QStringLiteral("FakeID");
}

QModelIndex JournalModel::adjustToFirstSplitIdx(const QModelIndex& index) const
{
    if (!index.isValid() || index.model() != this) {
        return {};
    }

    const auto id = index.data(eMyMoney::Model::IdRole).toString();

    // find the first split of this transaction in the journal
    QModelIndex idx;
    int startRow;
    for (startRow = index.row()-1; startRow >= 0; --startRow) {
        idx = index.model()->index(startRow, 0);
        const auto cid = idx.data(eMyMoney::Model::IdRole).toString();
        if (cid != id)
            break;
    }
    startRow++;

    return index.model()->index(startRow, index.column());
}
