/*
    SPDX-FileCopyrightText: 2003-2019 Thomas Baumgart <tbaumgart@kde.org>
    SPDX-FileCopyrightText: 2004 Ace Jones <acejones@users.sourceforge.net>
    SPDX-FileCopyrightText: 2008-2010 Alvaro Soliverez <asoliverez@gmail.com>
    SPDX-FileCopyrightText: 2017-2018 Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
    SPDX-License-Identifier: GPL-2.0-or-later
*/

#include "mymoneytransactionfilter.h"

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

#include <QDate>
#include <QFlags>
#include <QDebug>

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

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

#include "mymoneymoney.h"
#include "mymoneyfile.h"
#include "mymoneyaccount.h"
#include "mymoneysecurity.h"
#include "mymoneypayee.h"
#include "mymoneytag.h"
#include "mymoneytransaction.h"
#include "mymoneysplit.h"
#include "mymoneyenums.h"

class MyMoneyTransactionFilterPrivate {

public:
    MyMoneyTransactionFilterPrivate()
        : m_reportAllSplits(false)
        , m_considerCategory(false)
        , m_considerCategorySplits(false)
        , m_matchOnly(false)
        , m_treatTransfersAsIncomeExpense(false)
        , m_matchingSplitsCount(0)
        , m_invertText(false)
    {
    }

    MyMoneyTransactionFilter::FilterSet m_filterSet;
    bool                m_reportAllSplits;
    bool                m_considerCategory;
    bool                m_considerCategorySplits;
    bool                m_matchOnly;
    bool                m_treatTransfersAsIncomeExpense;

    uint                m_matchingSplitsCount;

    QRegExp             m_text;
    bool                m_invertText;
    QHash<QString, QString>    m_accounts;
    QHash<QString, QString>    m_payees;
    QHash<QString, QString>    m_tags;
    QHash<QString, QString>    m_categories;
    QHash<int, QString>      m_states;
    QHash<int, QString>      m_types;
    QHash<int, QString>      m_validity;
    QString             m_fromNr, m_toNr;
    QDate               m_fromDate, m_toDate;
    MyMoneyMoney        m_fromAmount, m_toAmount;
};

MyMoneyTransactionFilter::MyMoneyTransactionFilter() :
    d_ptr(new MyMoneyTransactionFilterPrivate)
{
    Q_D(MyMoneyTransactionFilter);
    d->m_reportAllSplits = true;
    d->m_considerCategory = true;
}

MyMoneyTransactionFilter::MyMoneyTransactionFilter(const QString& id) :
    d_ptr(new MyMoneyTransactionFilterPrivate)
{
    addAccount(id);
}

MyMoneyTransactionFilter::MyMoneyTransactionFilter(const MyMoneyTransactionFilter& other) :
    d_ptr(new MyMoneyTransactionFilterPrivate(*other.d_func()))
{
}

MyMoneyTransactionFilter::~MyMoneyTransactionFilter()
{
    Q_D(MyMoneyTransactionFilter);
    delete d;
}

void MyMoneyTransactionFilter::clear()
{
    Q_D(MyMoneyTransactionFilter);
    d->m_filterSet = {};
    d->m_invertText = false;
    d->m_accounts.clear();
    d->m_categories.clear();
    d->m_payees.clear();
    d->m_tags.clear();
    d->m_types.clear();
    d->m_states.clear();
    d->m_validity.clear();
    d->m_fromDate = QDate();
    d->m_toDate = QDate();
}

void MyMoneyTransactionFilter::clearAccountFilter()
{
    Q_D(MyMoneyTransactionFilter);
    d->m_filterSet.setFlag(accountFilterActive);
    d->m_accounts.clear();
}

void MyMoneyTransactionFilter::setTextFilter(const QRegExp& text, bool invert)
{
    Q_D(MyMoneyTransactionFilter);
    d->m_filterSet.setFlag(textFilterActive);
    d->m_invertText = invert;
    d->m_text = text;
}

void MyMoneyTransactionFilter::addAccount(const QStringList& ids)
{
    Q_D(MyMoneyTransactionFilter);

    d->m_filterSet.setFlag(accountFilterActive);
    for (const auto& id : ids)
        addAccount(id);
}

void MyMoneyTransactionFilter::addAccount(const QString& id)
{
    Q_D(MyMoneyTransactionFilter);
    if (!d->m_accounts.isEmpty() && !id.isEmpty() &&
            d->m_accounts.contains(id))
        return;

    d->m_filterSet.setFlag(accountFilterActive);
    if (!id.isEmpty())
        d->m_accounts.insert(id, QString());
}

void MyMoneyTransactionFilter::addCategory(const QStringList& ids)
{
    Q_D(MyMoneyTransactionFilter);

    d->m_filterSet.setFlag(categoryFilterActive);
    for (const auto& id : ids)
        addCategory(id);
}

void MyMoneyTransactionFilter::addCategory(const QString& id)
{
    Q_D(MyMoneyTransactionFilter);
    if (!d->m_categories.isEmpty() && !id.isEmpty() &&
            d->m_categories.contains(id))
        return;

    d->m_filterSet.setFlag(categoryFilterActive);
    if (!id.isEmpty())
        d->m_categories.insert(id, QString());
}

void MyMoneyTransactionFilter::setDateFilter(const QDate& from, const QDate& to)
{
    Q_D(MyMoneyTransactionFilter);
    d->m_filterSet.setFlag(dateFilterActive, from.isValid() | to.isValid());
    d->m_fromDate = from;
    d->m_toDate = to;
}

void MyMoneyTransactionFilter::setAmountFilter(const MyMoneyMoney& from, const MyMoneyMoney& to)
{
    Q_D(MyMoneyTransactionFilter);
    d->m_filterSet.setFlag(amountFilterActive);
    d->m_fromAmount = from.abs();
    d->m_toAmount = to.abs();

    // make sure that the user does not try to fool us  ;-)
    if (from > to)
        std::swap(d->m_fromAmount, d->m_toAmount);
}

void MyMoneyTransactionFilter::addPayee(const QString& id)
{
    Q_D(MyMoneyTransactionFilter);
    if (!d->m_payees.isEmpty() && !id.isEmpty() &&
            d->m_payees.contains(id))
        return;

    d->m_filterSet.setFlag(payeeFilterActive);
    if (!id.isEmpty())
        d->m_payees.insert(id, QString());
}

void MyMoneyTransactionFilter::addTag(const QString& id)
{
    Q_D(MyMoneyTransactionFilter);
    if (!d->m_tags.isEmpty() && !id.isEmpty() &&
            d->m_tags.contains(id))
        return;

    d->m_filterSet.setFlag(tagFilterActive);
    if (!id.isEmpty())
        d->m_tags.insert(id, QString());
}

void MyMoneyTransactionFilter::addType(const int type)
{
    Q_D(MyMoneyTransactionFilter);
    if (!d->m_types.isEmpty() &&
            d->m_types.contains(type))
        return;

    d->m_filterSet.setFlag(typeFilterActive);
    d->m_types.insert(type, QString());
}

void MyMoneyTransactionFilter::addState(const int state)
{
    Q_D(MyMoneyTransactionFilter);
    if (!d->m_states.isEmpty() &&
            d->m_states.contains(state))
        return;

    d->m_filterSet.setFlag(stateFilterActive);
    d->m_states.insert(state, QString());
}

void MyMoneyTransactionFilter::addValidity(const int type)
{
    Q_D(MyMoneyTransactionFilter);
    if (!d->m_validity.isEmpty() &&
            d->m_validity.contains(type))
        return;

    d->m_filterSet.setFlag(validityFilterActive);
    d->m_validity.insert(type, QString());
}

void MyMoneyTransactionFilter::setNumberFilter(const QString& from, const QString& to)
{
    Q_D(MyMoneyTransactionFilter);
    d->m_filterSet.setFlag(nrFilterActive);
    d->m_fromNr = from;
    d->m_toNr = to;
}

void MyMoneyTransactionFilter::setReportAllSplits(const bool report)
{
    Q_D(MyMoneyTransactionFilter);
    d->m_reportAllSplits = report;
}

void MyMoneyTransactionFilter::setConsiderCategorySplits(const bool check)
{
    Q_D(MyMoneyTransactionFilter);
    d->m_considerCategorySplits = check;
}

void MyMoneyTransactionFilter::setConsiderCategory(const bool check)
{
    Q_D(MyMoneyTransactionFilter);
    d->m_considerCategory = check;
}

void MyMoneyTransactionFilter::setTreatTransfersAsIncomeExpense(const bool check)
{
    Q_D(MyMoneyTransactionFilter);
    d->m_treatTransfersAsIncomeExpense = check;
}

bool MyMoneyTransactionFilter::treatTransfersAsIncomeExpense() const
{
    Q_D(const MyMoneyTransactionFilter);
    return d->m_treatTransfersAsIncomeExpense;
}

uint MyMoneyTransactionFilter::matchingSplitsCount(const MyMoneyTransaction& transaction)
{
    Q_D(MyMoneyTransactionFilter);
    d->m_matchOnly = true;
    matchingSplits(transaction);
    d->m_matchOnly = false;
    return d->m_matchingSplitsCount;
}

QVector<MyMoneySplit> MyMoneyTransactionFilter::matchingSplits(const MyMoneyTransaction& transaction)
{
    Q_D(MyMoneyTransactionFilter);

    QVector<MyMoneySplit> matchingSplits;
    const auto file = MyMoneyFile::instance();

    // qDebug("T: %s", transaction.id().data());
    // if no filter is set, we can safely return a match
    // if we should report all splits, then we collect them
    if (!d->m_filterSet && d->m_reportAllSplits) {
        d->m_matchingSplitsCount = transaction.splitCount();
        if (!d->m_matchOnly)
            matchingSplits = QVector<MyMoneySplit>::fromList(transaction.splits());
        return matchingSplits;
    }

    d->m_matchingSplitsCount = 0;
    const auto filter = d->m_filterSet;

    // perform checks on the MyMoneyTransaction object first

    // check the date range
    if (filter & dateFilterActive) {
        if ((d->m_fromDate != QDate() &&
                transaction.postDate() < d->m_fromDate) ||
                (d->m_toDate != QDate() &&
                 transaction.postDate() > d->m_toDate)) {
            return matchingSplits;
        }
    }

    auto categoryMatched = !(filter.testFlag(categoryFilterActive));
    auto accountMatched = !(filter.testFlag(accountFilterActive));
    auto isTransfer = true;

    // check the transaction's validity
    if (filter & validityFilterActive) {
        if (!d->m_validity.isEmpty() &&
                !d->m_validity.contains((int)validTransaction(transaction)))
            return matchingSplits;
    }

    // if d->m_reportAllSplits == false..
    // ...then we don't need splits...
    // ...but we need to know if there were any found
    auto isMatchingSplitsEmpty = true;

    auto extendedFilter = d->m_filterSet;
    extendedFilter.setFlag(dateFilterActive, false);
    extendedFilter.setFlag(accountFilterActive, false);
    extendedFilter.setFlag(categoryFilterActive, false);

    const auto needAccountMatch = filter.testFlag(accountFilterActive);
    const auto needCategoryMatch = filter.testFlag(categoryFilterActive);
    if (needAccountMatch || needCategoryMatch ||
            extendedFilter != 0) {
        const auto& splits = transaction.splits();
        for (const auto& s : splits) {
            if (needAccountMatch || needCategoryMatch) {
                auto removeSplit = true;
                if (d->m_considerCategory) {
                    switch (file->account(s.accountId()).accountGroup()) {
                    case eMyMoney::Account::Type::Income:
                    case eMyMoney::Account::Type::Expense:
                        isTransfer = false;
                        // check if the split references one of the categories in the list
                        if (needCategoryMatch) {
                            if (d->m_categories.isEmpty()) {
                                // we're looking for transactions with 'no' categories
                                d->m_matchingSplitsCount = 0;
                                matchingSplits.clear();
                                return matchingSplits;
                            } else if (d->m_categories.contains(s.accountId())) {
                                categoryMatched = true;
                                removeSplit = false;
                            }
                        }
                        break;

                    default:
                        // check if the split references one of the accounts in the list
                        if (!filter.testFlag(accountFilterActive)) {
                            removeSplit = false;
                        } else if (!d->m_accounts.isEmpty() &&
                                   d->m_accounts.contains(s.accountId())) {
                            accountMatched = true;
                            removeSplit = false;
                        }

                        break;
                    }

                } else {
                    if (!filter.testFlag(accountFilterActive)) {
                        removeSplit = false;
                    } else if (!d->m_accounts.isEmpty() &&
                               d->m_accounts.contains(s.accountId())) {
                        accountMatched = true;
                        removeSplit = false;
                    }
                }

                if (removeSplit)
                    continue;
            }

            // check if less frequent filters are active
            if (extendedFilter != 0) {
                const auto acc = file->account(s.accountId());
                if (!(matchAmount(s) && matchText(s, acc)))
                    continue;

                // Determine if this account is a category or an account
                auto isCategory = false;
                switch (acc.accountGroup()) {
                case eMyMoney::Account::Type::Income:
                case eMyMoney::Account::Type::Expense:
                    isCategory = true;
                default:
                    break;
                }

                bool includeSplit = d->m_considerCategorySplits || (!d->m_considerCategorySplits && !isCategory);
                if (includeSplit) {
                    // check the payee list
                    if (filter.testFlag(payeeFilterActive)) {
                        if (!d->m_payees.isEmpty()) {
                            if (s.payeeId().isEmpty() || !d->m_payees.contains(s.payeeId()))
                                continue;
                        } else if (!s.payeeId().isEmpty())
                            continue;
                    }

                    // check the tag list
                    if (filter.testFlag(tagFilterActive)) {
                        const auto tags = s.tagIdList();
                        if (!d->m_tags.isEmpty()) {
                            if (tags.isEmpty()) {
                                continue;
                            } else {
                                auto found = false;
                                for (const auto& tag : tags) {
                                    if (d->m_tags.contains(tag)) {
                                        found = true;
                                        break;
                                    }
                                }

                                if (!found)
                                    continue;
                            }
                        } else if (!tags.isEmpty())
                            continue;
                    }

                    // check the type list
                    if (filter.testFlag(typeFilterActive) &&
                            !d->m_types.isEmpty() &&
                            !d->m_types.contains(splitType(transaction, s, acc)))
                        continue;

                    // check the state list
                    if (filter.testFlag(stateFilterActive) &&
                            !d->m_states.isEmpty() &&
                            !d->m_states.contains(splitState(s)))
                        continue;

                    if (filter.testFlag(nrFilterActive) &&
                            ((!d->m_fromNr.isEmpty() && s.number() < d->m_fromNr) ||
                             (!d->m_toNr.isEmpty() && s.number() > d->m_toNr)))
                        continue;

                } else if (filter & (payeeFilterActive | tagFilterActive | typeFilterActive | stateFilterActive | nrFilterActive)) {
                    continue;
                }
            }
            if (d->m_reportAllSplits)
                matchingSplits.append(s);

            isMatchingSplitsEmpty = false;
        }

        // check if we're looking for transactions without assigned category
        if (!categoryMatched && transaction.splitCount() == 1 && d->m_categories.isEmpty())
            categoryMatched = true;

        if ((needAccountMatch && !accountMatched) ||
                (needCategoryMatch && !categoryMatched)) {
            matchingSplits.clear();
            return matchingSplits;
        }
    } else if (d->m_reportAllSplits) {
        const auto& splits = transaction.splits();
        for (const auto& s : splits)
            matchingSplits.append(s);
        d->m_matchingSplitsCount = matchingSplits.count();
        return matchingSplits;
    } else if (transaction.splitCount() > 0) {
        isMatchingSplitsEmpty = false;
    }

    // check if we're looking for transactions without assigned category
    if (!categoryMatched && transaction.splitCount() == 1 && d->m_categories.isEmpty())
        categoryMatched = true;

    // if there's no category filter and the category did not
    // match, then we still want to see this transaction if it's
    // a transfer
    if (!categoryMatched && !filter.testFlag(categoryFilterActive))
        categoryMatched = isTransfer;

    if (isMatchingSplitsEmpty || !(accountMatched && categoryMatched)) {
        d->m_matchingSplitsCount = 0;
        return matchingSplits;
    }

    if (!d->m_reportAllSplits && !isMatchingSplitsEmpty) {
        d->m_matchingSplitsCount = 1;
        if (!d->m_matchOnly)
            matchingSplits.append(transaction.firstSplit());
    } else {
        d->m_matchingSplitsCount = matchingSplits.count();
    }

    // all filters passed, I guess we have a match
    // qDebug("  C: %d", m_matchingSplits.count());
    return matchingSplits;
}

QDate MyMoneyTransactionFilter::fromDate() const
{
    Q_D(const MyMoneyTransactionFilter);
    return d->m_fromDate;
}

QDate MyMoneyTransactionFilter::toDate() const
{
    Q_D(const MyMoneyTransactionFilter);
    return d->m_toDate;
}

bool MyMoneyTransactionFilter::matchText(const MyMoneySplit& s, const MyMoneyAccount& acc) const
{
    Q_D(const MyMoneyTransactionFilter);
    // check if the text is contained in one of the fields
    // memo, value, number, payee, tag, account
    if (d->m_filterSet & textFilterActive) {
        const auto file = MyMoneyFile::instance();
        const auto sec = file->security(acc.currencyId());
        if (s.memo().contains(d->m_text) ||
                s.shares().formatMoney(acc.fraction(sec)).contains(d->m_text) ||
                s.value().formatMoney(acc.fraction(sec)).contains(d->m_text) ||
                s.number().contains(d->m_text) ||
                (d->m_text.pattern().compare(s.transactionId())) == 0)
            return !d->m_invertText;

        if (acc.name().contains(d->m_text))
            return !d->m_invertText;

        if (!s.payeeId().isEmpty() && file->payee(s.payeeId()).name().contains(d->m_text))
            return !d->m_invertText;

        const auto& tagIdList = s.tagIdList();
        for (const auto& tag : tagIdList)
            if (file->tag(tag).name().contains(d->m_text))
                return !d->m_invertText;

        return d->m_invertText;
    }
    return true;
}

bool MyMoneyTransactionFilter::matchAmount(const MyMoneySplit& s) const
{
    Q_D(const MyMoneyTransactionFilter);
    if (d->m_filterSet & amountFilterActive) {
        const auto value  = s.value().abs();
        const auto shares = s.shares().abs();
        if ((value  < d->m_fromAmount || value  > d->m_toAmount) &&
                (shares < d->m_fromAmount || shares > d->m_toAmount))
            return false;
    }

    return true;
}

bool MyMoneyTransactionFilter::match(const MyMoneySplit& s) const
{
    const auto& acc = MyMoneyFile::instance()->account(s.accountId());
    return matchText(s, acc) && matchAmount(s);
}

bool MyMoneyTransactionFilter::match(const MyMoneyTransaction& transaction)
{
    Q_D(MyMoneyTransactionFilter);
    d->m_matchOnly = true;
    matchingSplits(transaction);
    d->m_matchOnly = false;
    return d->m_matchingSplitsCount > 0;
}

int MyMoneyTransactionFilter::splitState(const MyMoneySplit& split) const
{
    switch (split.reconcileFlag()) {
    default:
    case eMyMoney::Split::State::NotReconciled:
        return (int)eMyMoney::TransactionFilter::State::NotReconciled;
    case eMyMoney::Split::State::Cleared:
        return (int)eMyMoney::TransactionFilter::State::Cleared;
    case eMyMoney::Split::State::Reconciled:
        return (int)eMyMoney::TransactionFilter::State::Reconciled;
    case eMyMoney::Split::State::Frozen:
        return (int)eMyMoney::TransactionFilter::State::Frozen;
    }
}

int MyMoneyTransactionFilter::splitType(const MyMoneyTransaction& t, const MyMoneySplit& split, const MyMoneyAccount& acc) const
{
    Q_D(const MyMoneyTransactionFilter);
    if (acc.isIncomeExpense())
        return (int)eMyMoney::TransactionFilter::Type::All;

    if (t.splitCount() == 2 && !d->m_treatTransfersAsIncomeExpense) {
        const auto& splits = t.splits();
        const auto file = MyMoneyFile::instance();
        const auto& a = splits.at(0).id().compare(split.id()) == 0 ? acc : file->account(splits.at(0).accountId());
        const auto& b = splits.at(1).id().compare(split.id()) == 0 ? acc : file->account(splits.at(1).accountId());

        if (!a.isIncomeExpense() && !b.isIncomeExpense())
            return (int)eMyMoney::TransactionFilter::Type::Transfers;
    }

    if (split.value().isPositive())
        return (int)eMyMoney::TransactionFilter::Type::Deposits;

    return (int)eMyMoney::TransactionFilter::Type::Payments;
}

eMyMoney::TransactionFilter::Validity MyMoneyTransactionFilter::validTransaction(const MyMoneyTransaction& t) const
{
    MyMoneyMoney val;
    const auto& splits = t.splits();
    for (const auto& split : splits)
        val += split.value();

    return (val == MyMoneyMoney()) ? eMyMoney::TransactionFilter::Validity::Valid : eMyMoney::TransactionFilter::Validity::Invalid;
}

bool MyMoneyTransactionFilter::includesCategory(const QString& cat) const
{
    Q_D(const MyMoneyTransactionFilter);
    return !d->m_filterSet.testFlag(categoryFilterActive) || d->m_categories.contains(cat);
}

bool MyMoneyTransactionFilter::includesAccount(const QString& acc) const
{
    Q_D(const MyMoneyTransactionFilter);
    return !d->m_filterSet.testFlag(accountFilterActive) || d->m_accounts.contains(acc);
}

bool MyMoneyTransactionFilter::includesPayee(const QString& pye) const
{
    Q_D(const MyMoneyTransactionFilter);
    return !d->m_filterSet.testFlag(payeeFilterActive) || d->m_payees.contains(pye);
}

bool MyMoneyTransactionFilter::includesTag(const QString& tag) const
{
    Q_D(const MyMoneyTransactionFilter);
    return !d->m_filterSet.testFlag(tagFilterActive) || d->m_tags.contains(tag);
}

bool MyMoneyTransactionFilter::dateFilter(QDate& from, QDate& to) const
{
    Q_D(const MyMoneyTransactionFilter);
    from = d->m_fromDate;
    to = d->m_toDate;
    return d->m_filterSet.testFlag(dateFilterActive);
}

bool MyMoneyTransactionFilter::amountFilter(MyMoneyMoney& from, MyMoneyMoney& to) const
{
    Q_D(const MyMoneyTransactionFilter);
    from = d->m_fromAmount;
    to = d->m_toAmount;
    return d->m_filterSet.testFlag(amountFilterActive);
}

bool MyMoneyTransactionFilter::numberFilter(QString& from, QString& to) const
{
    Q_D(const MyMoneyTransactionFilter);
    from = d->m_fromNr;
    to = d->m_toNr;
    return d->m_filterSet.testFlag(nrFilterActive);
}

bool MyMoneyTransactionFilter::payees(QStringList& list) const
{
    Q_D(const MyMoneyTransactionFilter);
    auto result = d->m_filterSet.testFlag(payeeFilterActive);

    if (result) {
        QHashIterator<QString, QString> it_payee(d->m_payees);
        while (it_payee.hasNext()) {
            it_payee.next();
            list += it_payee.key();
        }
    }
    return result;
}

bool MyMoneyTransactionFilter::tags(QStringList& list) const
{
    Q_D(const MyMoneyTransactionFilter);
    auto result = d->m_filterSet.testFlag(tagFilterActive);

    if (result) {
        QHashIterator<QString, QString> it_tag(d->m_tags);
        while (it_tag.hasNext()) {
            it_tag.next();
            list += it_tag.key();
        }
    }
    return result;
}

bool MyMoneyTransactionFilter::accounts(QStringList& list) const
{
    Q_D(const MyMoneyTransactionFilter);
    auto result = d->m_filterSet.testFlag(accountFilterActive);

    if (result) {
        QHashIterator<QString, QString> it_account(d->m_accounts);
        while (it_account.hasNext()) {
            it_account.next();
            QString account = it_account.key();
            list += account;
        }
    }
    return result;
}

bool MyMoneyTransactionFilter::categories(QStringList& list) const
{
    Q_D(const MyMoneyTransactionFilter);
    auto result = d->m_filterSet.testFlag(categoryFilterActive);

    if (result) {
        QHashIterator<QString, QString> it_category(d->m_categories);
        while (it_category.hasNext()) {
            it_category.next();
            list += it_category.key();
        }
    }
    return result;
}

bool MyMoneyTransactionFilter::types(QList<int>& list) const
{
    Q_D(const MyMoneyTransactionFilter);
    auto result = d->m_filterSet.testFlag(typeFilterActive);

    if (result) {
        QHashIterator<int, QString> it_type(d->m_types);
        while (it_type.hasNext()) {
            it_type.next();
            list += it_type.key();
        }
    }
    return result;
}

bool MyMoneyTransactionFilter::states(QList<int>& list) const
{
    Q_D(const MyMoneyTransactionFilter);
    auto result = d->m_filterSet.testFlag(stateFilterActive);

    if (result) {
        QHashIterator<int, QString> it_state(d->m_states);
        while (it_state.hasNext()) {
            it_state.next();
            list += it_state.key();
        }
    }
    return result;
}

bool MyMoneyTransactionFilter::validities(QList<int>& list) const
{
    Q_D(const MyMoneyTransactionFilter);
    auto result = d->m_filterSet.testFlag(validityFilterActive);

    if (result) {
        QHashIterator<int, QString> it_validity(d->m_validity);
        while (it_validity.hasNext()) {
            it_validity.next();
            list += it_validity.key();
        }
    }
    return result;
}

bool MyMoneyTransactionFilter::firstType(int&i) const
{
    Q_D(const MyMoneyTransactionFilter);
    auto result = d->m_filterSet.testFlag(typeFilterActive);

    if (result) {
        QHashIterator<int, QString> it_type(d->m_types);
        if (it_type.hasNext()) {
            it_type.next();
            i = it_type.key();
        }
    }
    return result;
}

bool MyMoneyTransactionFilter::firstState(int&i) const
{
    Q_D(const MyMoneyTransactionFilter);
    auto result = d->m_filterSet.testFlag(stateFilterActive);

    if (result) {
        QHashIterator<int, QString> it_state(d->m_states);
        if (it_state.hasNext()) {
            it_state.next();
            i = it_state.key();
        }
    }
    return result;
}

bool MyMoneyTransactionFilter::firstValidity(int&i) const
{
    Q_D(const MyMoneyTransactionFilter);
    auto result = d->m_filterSet.testFlag(validityFilterActive);

    if (result) {
        QHashIterator<int, QString> it_validity(d->m_validity);
        if (it_validity.hasNext()) {
            it_validity.next();
            i = it_validity.key();
        }
    }
    return result;
}

bool MyMoneyTransactionFilter::textFilter(QRegExp& exp) const
{
    Q_D(const MyMoneyTransactionFilter);
    exp = d->m_text;
    return d->m_filterSet.testFlag(textFilterActive);
}

bool MyMoneyTransactionFilter::isInvertingText() const
{
    Q_D(const MyMoneyTransactionFilter);
    return d->m_invertText;
}

void MyMoneyTransactionFilter::setDateFilter(eMyMoney::TransactionFilter::Date range)
{
    QDate from, to;
    if (translateDateRange(range, from, to))
        setDateFilter(from, to);
}

static int fiscalYearStartMonth = 1;
static int fiscalYearStartDay = 1;

void MyMoneyTransactionFilter::setFiscalYearStart(int firstMonth, int firstDay)
{
    fiscalYearStartMonth = firstMonth;
    fiscalYearStartDay = firstDay;
}

bool MyMoneyTransactionFilter::translateDateRange(eMyMoney::TransactionFilter::Date id, QDate& start, QDate& end)
{
    bool rc = true;
    int yr = QDate::currentDate().year();
    int mon = QDate::currentDate().month();

    switch (id) {
    case eMyMoney::TransactionFilter::Date::All:
        start = QDate();
        end = QDate();
        break;
    case eMyMoney::TransactionFilter::Date::AsOfToday:
        start = QDate();
        end =  QDate::currentDate();
        break;
    case eMyMoney::TransactionFilter::Date::CurrentMonth:
        start = QDate(yr, mon, 1);
        end = QDate(yr, mon, 1).addMonths(1).addDays(-1);
        break;
    case eMyMoney::TransactionFilter::Date::CurrentYear:
        start = QDate(yr, 1, 1);
        end = QDate(yr, 12, 31);
        break;
    case eMyMoney::TransactionFilter::Date::MonthToDate:
        start = QDate(yr, mon, 1);
        end = QDate::currentDate();
        break;
    case eMyMoney::TransactionFilter::Date::YearToDate:
        start = QDate(yr, 1, 1);
        end = QDate::currentDate();
        break;
    case eMyMoney::TransactionFilter::Date::YearToMonth:
        start = QDate(yr, 1, 1);
        end = QDate(yr, mon, 1).addDays(-1);
        break;
    case eMyMoney::TransactionFilter::Date::LastMonth:
        start = QDate(yr, mon, 1).addMonths(-1);
        end = QDate(yr, mon, 1).addDays(-1);
        break;
    case eMyMoney::TransactionFilter::Date::LastYear:
        start = QDate(yr, 1, 1).addYears(-1);
        end = QDate(yr, 12, 31).addYears(-1);
        break;
    case eMyMoney::TransactionFilter::Date::Last7Days:
        start = QDate::currentDate().addDays(-7);
        end = QDate::currentDate();
        break;
    case eMyMoney::TransactionFilter::Date::Last30Days:
        start = QDate::currentDate().addDays(-30);
        end = QDate::currentDate();
        break;
    case eMyMoney::TransactionFilter::Date::Last3Months:
        start = QDate::currentDate().addMonths(-3);
        end = QDate::currentDate();
        break;
    case eMyMoney::TransactionFilter::Date::Last6Months:
        start = QDate::currentDate().addMonths(-6);
        end = QDate::currentDate();
        break;
    case eMyMoney::TransactionFilter::Date::Last11Months:
        start = QDate(yr, mon, 1).addMonths(-12);
        end = QDate(yr, mon, 1).addDays(-1);
        break;
    case eMyMoney::TransactionFilter::Date::Last12Months:
        start = QDate::currentDate().addMonths(-12);
        end = QDate::currentDate();
        break;
    case eMyMoney::TransactionFilter::Date::Next7Days:
        start = QDate::currentDate();
        end = QDate::currentDate().addDays(7);
        break;
    case eMyMoney::TransactionFilter::Date::Next30Days:
        start = QDate::currentDate();
        end = QDate::currentDate().addDays(30);
        break;
    case eMyMoney::TransactionFilter::Date::Next3Months:
        start = QDate::currentDate();
        end = QDate::currentDate().addMonths(3);
        break;
    case eMyMoney::TransactionFilter::Date::Next6Months:
        start = QDate::currentDate();
        end = QDate::currentDate().addMonths(6);
        break;
    case eMyMoney::TransactionFilter::Date::Next12Months:
        start = QDate::currentDate();
        end = QDate::currentDate().addMonths(12);
        break;
    case eMyMoney::TransactionFilter::Date::Next18Months:
        start = QDate::currentDate();
        end = QDate::currentDate().addMonths(18);
        break;
    case eMyMoney::TransactionFilter::Date::UserDefined:
        start = QDate();
        end = QDate();
        break;
    case eMyMoney::TransactionFilter::Date::Last3ToNext3Months:
        start = QDate::currentDate().addMonths(-3);
        end = QDate::currentDate().addMonths(3);
        break;
    case eMyMoney::TransactionFilter::Date::CurrentQuarter:
        start = QDate(yr, mon - ((mon - 1) % 3), 1);
        end = start.addMonths(3).addDays(-1);
        break;
    case eMyMoney::TransactionFilter::Date::LastQuarter:
        start = QDate(yr, mon - ((mon - 1) % 3), 1).addMonths(-3);
        end = start.addMonths(3).addDays(-1);
        break;
    case eMyMoney::TransactionFilter::Date::NextQuarter:
        start = QDate(yr, mon - ((mon - 1) % 3), 1).addMonths(3);
        end = start.addMonths(3).addDays(-1);
        break;
    case eMyMoney::TransactionFilter::Date::CurrentFiscalYear:
        start = QDate(QDate::currentDate().year(), fiscalYearStartMonth, fiscalYearStartDay);
        if (QDate::currentDate() < start)
            start = start.addYears(-1);
        end = start.addYears(1).addDays(-1);
        break;
    case eMyMoney::TransactionFilter::Date::LastFiscalYear:
        start = QDate(QDate::currentDate().year(), fiscalYearStartMonth, fiscalYearStartDay);
        if (QDate::currentDate() < start)
            start = start.addYears(-1);
        start = start.addYears(-1);
        end = start.addYears(1).addDays(-1);
        break;
    case eMyMoney::TransactionFilter::Date::Today:
        start = QDate::currentDate();
        end =  QDate::currentDate();
        break;
    default:
        qWarning("Unknown date identifier %d in MyMoneyTransactionFilter::translateDateRange()", (int)id);
        rc = false;
        break;
    }
    return rc;
}

MyMoneyTransactionFilter::FilterSet MyMoneyTransactionFilter::filterSet() const
{
    Q_D(const MyMoneyTransactionFilter);
    return d->m_filterSet;
}

void MyMoneyTransactionFilter::removeReference(const QString& id)
{
    Q_D(MyMoneyTransactionFilter);
    if (d->m_accounts.end() != d->m_accounts.find(id)) {
        qDebug("%s", qPrintable(QString("Remove account '%1' from report").arg(id)));
        d->m_accounts.take(id);
    } else if (d->m_categories.end() != d->m_categories.find(id)) {
        qDebug("%s", qPrintable(QString("Remove category '%1' from report").arg(id)));
        d->m_categories.remove(id);
    } else if (d->m_payees.end() != d->m_payees.find(id)) {
        qDebug("%s", qPrintable(QString("Remove payee '%1' from report").arg(id)));
        d->m_payees.remove(id);
    } else if (d->m_tags.end() != d->m_tags.find(id)) {
        qDebug("%s", qPrintable(QString("Remove tag '%1' from report").arg(id)));
        d->m_tags.remove(id);
    }
}
