/********************************************************************\
 * Account.c -- Account data structure implementation               *
 * Copyright (C) 1997 Robin D. Clark                                *
 * Copyright (C) 1997-2003 Linas Vepstas <linas@linas.org>          *
 * Copyright (C) 2007 David Hampton <hampton@employees.org>         *
 *                                                                  *
 * This program is free software; you can redistribute it and/or    *
 * modify it under the terms of the GNU General Public License as   *
 * published by the Free Software Foundation; either version 2 of   *
 * the License, or (at your option) any later version.              *
 *                                                                  *
 * This program is distributed in the hope that it will be useful,  *
 * but WITHOUT ANY WARRANTY; without even the implied warranty of   *
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the    *
 * GNU General Public License for more details.                     *
 *                                                                  *
 * You should have received a copy of the GNU General Public License*
 * along with this program; if not, contact:                        *
 *                                                                  *
 * Free Software Foundation           Voice:  +1-617-542-5942       *
 * 51 Franklin Street, Fifth Floor    Fax:    +1-617-542-2652       *
 * Boston, MA  02110-1301,  USA       gnu@gnu.org                   *
 *                                                                  *
\********************************************************************/

#include <config.h>

#include "gnc-prefs.h"

#include <glib.h>
#include <glib/gi18n.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>

#include "AccountP.hpp"
#include "Account.hpp"
#include "Split.h"
#include "Transaction.h"
#include "TransactionP.hpp"
#include "gnc-event.h"
#include "gnc-glib-utils.h"
#include "gnc-lot.h"
#include "gnc-pricedb.h"
#include "qofinstance-p.h"
#include "gnc-features.h"
#include "guid.hpp"

#include <numeric>
#include <map>
#include <unordered_set>

static QofLogModule log_module = GNC_MOD_ACCOUNT;

/* The Canonical Account Separator.  Pre-Initialized. */
static gchar account_separator[8] = ".";
static gunichar account_uc_separator = ':';

static bool imap_convert_bayes_to_flat_run = false;

/* Predefined KVP paths */
static const std::string KEY_ASSOC_INCOME_ACCOUNT("ofx/associated-income-account");
static const std::string KEY_RECONCILE_INFO("reconcile-info");
static const std::string KEY_INCLUDE_CHILDREN("include-children");
static const std::string KEY_POSTPONE("postpone");
static const std::string KEY_LOT_MGMT("lot-mgmt");
static const std::string KEY_ONLINE_ID("online_id");
static const std::string KEY_IMP_APPEND_TEXT("import-append-text");
static const std::string AB_KEY("hbci");
static const std::string AB_ACCOUNT_ID("account-id");
static const std::string AB_ACCOUNT_UID("account-uid");
static const std::string AB_BANK_CODE("bank-code");
static const std::string AB_TRANS_RETRIEVAL("trans-retrieval");

static const std::string KEY_BALANCE_LIMIT("balance-limit");
static const std::string KEY_BALANCE_HIGHER_LIMIT_VALUE("higher-value");
static const std::string KEY_BALANCE_LOWER_LIMIT_VALUE("lower-value");
static const std::string KEY_BALANCE_INCLUDE_SUB_ACCTS("inlude-sub-accts");

using FinalProbabilityVec=std::vector<std::pair<std::string, int32_t>>;
using ProbabilityVec=std::vector<std::pair<std::string, struct AccountProbability>>;
using FlatKvpEntry=std::pair<std::string, KvpValue*>;

enum
{
    LAST_SIGNAL
};

enum
{
    PROP_0,
    PROP_NAME,                          /* Table */
    PROP_FULL_NAME,                     /* Constructed */
    PROP_CODE,                          /* Table */
    PROP_DESCRIPTION,                   /* Table */
    PROP_COLOR,                         /* KVP */
    PROP_NOTES,                         /* KVP */
    PROP_TYPE,                          /* Table */

//    PROP_PARENT,                      /* Table, Not a property */
    PROP_COMMODITY,                     /* Table */
    PROP_COMMODITY_SCU,                 /* Table */
    PROP_NON_STD_SCU,                   /* Table */
    PROP_END_BALANCE,                   /* Constructed */
    PROP_END_NOCLOSING_BALANCE,         /* Constructed */
    PROP_END_CLEARED_BALANCE,           /* Constructed */
    PROP_END_RECONCILED_BALANCE,        /* Constructed */

    PROP_TAX_RELATED,                   /* KVP */
    PROP_TAX_CODE,                      /* KVP */
    PROP_TAX_SOURCE,                    /* KVP */
    PROP_TAX_COPY_NUMBER,               /* KVP */

    PROP_HIDDEN,                        /* Table slot exists, but in KVP in memory & xml */
    PROP_PLACEHOLDER,                   /* Table slot exists, but in KVP in memory & xml */
    PROP_AUTO_INTEREST,
    PROP_FILTER,                        /* KVP */
    PROP_SORT_ORDER,                    /* KVP */
    PROP_SORT_REVERSED,

    PROP_LOT_NEXT_ID,                   /* KVP */
    PROP_ONLINE_ACCOUNT,                /* KVP */
    PROP_IMP_APPEND_TEXT,               /* KVP */
    PROP_IS_OPENING_BALANCE,            /* KVP */
    PROP_OFX_INCOME_ACCOUNT,            /* KVP */
    PROP_AB_ACCOUNT_ID,                 /* KVP */
    PROP_AB_ACCOUNT_UID,                /* KVP */
    PROP_AB_BANK_CODE,                  /* KVP */
    PROP_AB_TRANS_RETRIEVAL,            /* KVP */

    PROP_RUNTIME_0,
    PROP_POLICY,                        /* Cached Value */
    PROP_MARK,                          /* Runtime Value */
    PROP_SORT_DIRTY,                    /* Runtime Value */
    PROP_BALANCE_DIRTY,                 /* Runtime Value */
    PROP_START_BALANCE,                 /* Runtime Value */
    PROP_START_NOCLOSING_BALANCE,       /* Runtime Value */
    PROP_START_CLEARED_BALANCE,         /* Runtime Value */
    PROP_START_RECONCILED_BALANCE,      /* Runtime Value */
};

#define GET_PRIVATE(o)  \
    ((AccountPrivate*)gnc_account_get_instance_private((Account*)o))

/* This map contains a set of strings representing the different column types. */
static const std::map<GNCAccountType, const char*> gnc_acct_debit_strs = {
    { ACCT_TYPE_NONE,       N_("Funds In") },
    { ACCT_TYPE_BANK,       N_("Deposit") },
    { ACCT_TYPE_CASH,       N_("Receive") },
    { ACCT_TYPE_CREDIT,     N_("Payment") },
    { ACCT_TYPE_ASSET,      N_("Increase") },
    { ACCT_TYPE_LIABILITY,  N_("Decrease") },
    { ACCT_TYPE_STOCK,      N_("Buy") },
    { ACCT_TYPE_MUTUAL,     N_("Buy") },
    { ACCT_TYPE_CURRENCY,   N_("Buy") },
    { ACCT_TYPE_INCOME,     N_("Charge") },
    { ACCT_TYPE_EXPENSE,    N_("Expense") },
    { ACCT_TYPE_PAYABLE,    N_("Payment") },
    { ACCT_TYPE_RECEIVABLE, N_("Invoice") },
    { ACCT_TYPE_TRADING,    N_("Decrease") },
    { ACCT_TYPE_EQUITY,     N_("Decrease") },
};
static const char* dflt_acct_debit_str = N_("Debit");

/* This map contains a set of strings representing the different column types. */
static const std::map<GNCAccountType, const char*> gnc_acct_credit_strs = {
    { ACCT_TYPE_NONE,       N_("Funds Out") },
    { ACCT_TYPE_BANK,       N_("Withdrawal") },
    { ACCT_TYPE_CASH,       N_("Spend") },
    { ACCT_TYPE_CREDIT,     N_("Charge") },
    { ACCT_TYPE_ASSET,      N_("Decrease") },
    { ACCT_TYPE_LIABILITY,  N_("Increase") },
    { ACCT_TYPE_STOCK,      N_("Sell") },
    { ACCT_TYPE_MUTUAL,     N_("Sell") },
    { ACCT_TYPE_CURRENCY,   N_("Sell") },
    { ACCT_TYPE_INCOME,     N_("Income") },
    { ACCT_TYPE_EXPENSE,    N_("Rebate") },
    { ACCT_TYPE_PAYABLE,    N_("Bill") },
    { ACCT_TYPE_RECEIVABLE, N_("Payment") },
    { ACCT_TYPE_TRADING,    N_("Increase") },
    { ACCT_TYPE_EQUITY,     N_("Increase") },
};
static const char* dflt_acct_credit_str = N_("Credit");

/********************************************************************\
 * Because I can't use C++ for this project, doesn't mean that I    *
 * can't pretend to!  These functions perform actions on the        *
 * account data structure, in order to encapsulate the knowledge    *
 * of the internals of the Account in one file.                     *
\********************************************************************/

static void xaccAccountBringUpToDate (Account *acc);


/********************************************************************\
 * gnc_get_account_separator                                        *
 *   returns the current account separator character                *
 *                                                                  *
 * Args: none                                                       *
 * Returns: account separator character                             *
 \*******************************************************************/
const gchar *
gnc_get_account_separator_string (void)
{
    return account_separator;
}

gunichar
gnc_get_account_separator (void)
{
    return account_uc_separator;
}

void
gnc_set_account_separator (const gchar *separator)
{
    gunichar uc;
    gint count;

    uc = g_utf8_get_char_validated(separator, -1);
    if ((uc == (gunichar) - 2) || (uc == (gunichar) - 1) || g_unichar_isalnum(uc))
    {
        account_uc_separator = ':';
        strcpy(account_separator, ":");
        return;
    }

    account_uc_separator = uc;
    count = g_unichar_to_utf8(uc, account_separator);
    account_separator[count] = '\0';
}

gchar *gnc_account_name_violations_errmsg (const gchar *separator, GList* invalid_account_names)
{
    gchar *message = nullptr;

    if ( !invalid_account_names )
        return nullptr;

    auto account_list {gnc_g_list_stringjoin (invalid_account_names, "\n")};

    /* Translators: The first %s will be the account separator character,
       the second %s is a list of account names.
       The resulting string will be displayed to the user if there are
       account names containing the separator character. */
    message = g_strdup_printf(
                  _("The separator character \"%s\" is used in one or more account names.\n\n"
                    "This will result in unexpected behaviour. "
                    "Either change the account names or choose another separator character.\n\n"
                    "Below you will find the list of invalid account names:\n"
                    "%s"), separator, account_list );
    g_free ( account_list );
    return message;
}

struct ViolationData
{
    GList *list;
    const gchar *separator;
};

static void
check_acct_name (Account *acct, gpointer user_data)
{
    auto cb {static_cast<ViolationData*>(user_data)};
    auto name {xaccAccountGetName (acct)};
    if (g_strstr_len (name, -1, cb->separator))
        cb->list = g_list_prepend (cb->list, g_strdup (name));
}

GList *gnc_account_list_name_violations (QofBook *book, const gchar *separator)
{
    g_return_val_if_fail (separator != nullptr, nullptr);
    if (!book) return nullptr;
    ViolationData cb = { nullptr, separator };
    gnc_account_foreach_descendant (gnc_book_get_root_account (book),
                                    (AccountCb)check_acct_name, &cb);
    return cb.list;
}

/********************************************************************\
\********************************************************************/

static inline void mark_account (Account *acc);
void
mark_account (Account *acc)
{
    qof_instance_set_dirty(&acc->inst);
}

/********************************************************************\
\********************************************************************/

/* GObject Initialization */
G_DEFINE_TYPE_WITH_PRIVATE(Account, gnc_account, QOF_TYPE_INSTANCE)

static void
gnc_account_init(Account* acc)
{
    AccountPrivate *priv;

    priv = GET_PRIVATE(acc);
    priv->parent   = nullptr;

    priv->accountName = qof_string_cache_insert("");
    priv->accountCode = qof_string_cache_insert("");
    priv->description = qof_string_cache_insert("");

    priv->type = ACCT_TYPE_NONE;

    priv->mark = 0;

    priv->policy = xaccGetFIFOPolicy();
    priv->lots = nullptr;

    priv->commodity = nullptr;
    priv->commodity_scu = 0;
    priv->non_standard_scu = FALSE;

    priv->balance = gnc_numeric_zero();
    priv->noclosing_balance = gnc_numeric_zero();
    priv->cleared_balance = gnc_numeric_zero();
    priv->reconciled_balance = gnc_numeric_zero();
    priv->starting_balance = gnc_numeric_zero();
    priv->starting_noclosing_balance = gnc_numeric_zero();
    priv->starting_cleared_balance = gnc_numeric_zero();
    priv->starting_reconciled_balance = gnc_numeric_zero();
    priv->balance_dirty = FALSE;

    new (&priv->children) AccountVec ();
    new (&priv->splits) SplitsVec ();
    priv->splits_hash = g_hash_table_new (g_direct_hash, g_direct_equal);
    priv->sort_dirty = FALSE;
}

static void
gnc_account_dispose (GObject *acctp)
{
    G_OBJECT_CLASS(gnc_account_parent_class)->dispose(acctp);
}

static void
gnc_account_finalize(GObject* acctp)
{
    G_OBJECT_CLASS(gnc_account_parent_class)->finalize(acctp);
}

/* Note that g_value_set_object() refs the object, as does
 * g_object_get(). But g_object_get() only unrefs once when it disgorges
 * the object, leaving an unbalanced ref, which leaks. So instead of
 * using g_value_set_object(), use g_value_take_object() which doesn't
 * ref the object when used in get_property().
 */
static void
gnc_account_get_property (GObject         *object,
                          guint            prop_id,
                          GValue          *value,
                          GParamSpec      *pspec)
{
    Account *account;
    AccountPrivate *priv;

    g_return_if_fail(GNC_IS_ACCOUNT(object));

    account = GNC_ACCOUNT(object);
    priv = GET_PRIVATE(account);
    switch (prop_id)
    {
    case PROP_NAME:
        g_value_set_string(value, priv->accountName);
        break;
    case PROP_FULL_NAME:
        g_value_take_string(value, gnc_account_get_full_name(account));
        break;
    case PROP_CODE:
        g_value_set_string(value, priv->accountCode);
        break;
    case PROP_DESCRIPTION:
        g_value_set_string(value, priv->description);
        break;
    case PROP_COLOR:
        g_value_set_string(value, xaccAccountGetColor(account));
        break;
    case PROP_NOTES:
        g_value_set_string(value, xaccAccountGetNotes(account));
        break;
    case PROP_TYPE:
        // NEED TO BE CONVERTED TO A G_TYPE_ENUM
        g_value_set_int(value, priv->type);
        break;
    case PROP_COMMODITY:
        g_value_take_object(value, priv->commodity);
        break;
    case PROP_COMMODITY_SCU:
        g_value_set_int(value, priv->commodity_scu);
        break;
    case PROP_NON_STD_SCU:
        g_value_set_boolean(value, priv->non_standard_scu);
        break;
    case PROP_SORT_DIRTY:
        g_value_set_boolean(value, priv->sort_dirty);
        break;
    case PROP_BALANCE_DIRTY:
        g_value_set_boolean(value, priv->balance_dirty);
        break;
    case PROP_START_BALANCE:
        g_value_set_boxed(value, &priv->starting_balance);
        break;
    case PROP_START_NOCLOSING_BALANCE:
        g_value_set_boxed(value, &priv->starting_noclosing_balance);
        break;
    case PROP_START_CLEARED_BALANCE:
        g_value_set_boxed(value, &priv->starting_cleared_balance);
        break;
    case PROP_START_RECONCILED_BALANCE:
        g_value_set_boxed(value, &priv->starting_reconciled_balance);
        break;
    case PROP_END_BALANCE:
        g_value_set_boxed(value, &priv->balance);
        break;
    case PROP_END_NOCLOSING_BALANCE:
        g_value_set_boxed(value, &priv->noclosing_balance);
        break;
    case PROP_END_CLEARED_BALANCE:
        g_value_set_boxed(value, &priv->cleared_balance);
        break;
    case PROP_END_RECONCILED_BALANCE:
        g_value_set_boxed(value, &priv->reconciled_balance);
        break;
    case PROP_POLICY:
        /* MAKE THIS A BOXED VALUE */
        g_value_set_pointer(value, priv->policy);
        break;
    case PROP_MARK:
        g_value_set_int(value, priv->mark);
        break;
    case PROP_TAX_RELATED:
        g_value_set_boolean(value, xaccAccountGetTaxRelated(account));
        break;
    case PROP_TAX_CODE:
        g_value_set_string(value, xaccAccountGetTaxUSCode(account));
        break;
    case PROP_TAX_SOURCE:
        g_value_set_string(value,
                           xaccAccountGetTaxUSPayerNameSource(account));
        break;
    case PROP_TAX_COPY_NUMBER:
        g_value_set_int64(value,
                          xaccAccountGetTaxUSCopyNumber(account));
        break;
    case PROP_HIDDEN:
        g_value_set_boolean(value, xaccAccountGetHidden(account));
        break;
    case PROP_AUTO_INTEREST:
        g_value_set_boolean (value, xaccAccountGetAutoInterest (account));
        break;
    case PROP_IS_OPENING_BALANCE:
        g_value_set_boolean(value, xaccAccountGetIsOpeningBalance(account));
        break;
    case PROP_PLACEHOLDER:
        g_value_set_boolean(value, xaccAccountGetPlaceholder(account));
        break;
    case PROP_FILTER:
        g_value_set_string(value, xaccAccountGetFilter(account));
        break;
    case PROP_SORT_ORDER:
        g_value_set_string(value, xaccAccountGetSortOrder(account));
        break;
    case PROP_SORT_REVERSED:
        g_value_set_boolean(value, xaccAccountGetSortReversed(account));
        break;
    case PROP_LOT_NEXT_ID:
        /* Pre-set the value in case the frame is empty */
        g_value_set_int64 (value, 0);
        qof_instance_get_path_kvp (QOF_INSTANCE (account), value, {KEY_LOT_MGMT, "next-id"});
        break;
    case PROP_ONLINE_ACCOUNT:
        qof_instance_get_path_kvp (QOF_INSTANCE (account), value, {KEY_ONLINE_ID});
        break;
    case PROP_IMP_APPEND_TEXT:
        g_value_set_boolean(value, xaccAccountGetAppendText(account));
        break;
    case PROP_OFX_INCOME_ACCOUNT:
        qof_instance_get_path_kvp (QOF_INSTANCE (account), value, {KEY_ASSOC_INCOME_ACCOUNT});
        break;
    case PROP_AB_ACCOUNT_ID:
        qof_instance_get_path_kvp (QOF_INSTANCE (account), value, {AB_KEY, AB_ACCOUNT_ID});
        break;
    case PROP_AB_ACCOUNT_UID:
        qof_instance_get_path_kvp (QOF_INSTANCE (account), value, {AB_KEY, AB_ACCOUNT_UID});
        break;
    case PROP_AB_BANK_CODE:
        qof_instance_get_path_kvp (QOF_INSTANCE (account), value, {AB_KEY, AB_BANK_CODE});
        break;
    case PROP_AB_TRANS_RETRIEVAL:
        qof_instance_get_path_kvp (QOF_INSTANCE (account), value, {AB_KEY, AB_TRANS_RETRIEVAL});
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static void
gnc_account_set_property (GObject         *object,
                          guint            prop_id,
                          const GValue    *value,
                          GParamSpec      *pspec)
{
    Account *account;
    gnc_numeric *number;
    g_return_if_fail(GNC_IS_ACCOUNT(object));
    account = GNC_ACCOUNT(object);
    if (prop_id < PROP_RUNTIME_0)
        g_assert (qof_instance_get_editlevel(account));

    switch (prop_id)
    {
    case PROP_NAME:
        xaccAccountSetName(account, g_value_get_string(value));
        break;
    case PROP_CODE:
        xaccAccountSetCode(account, g_value_get_string(value));
        break;
    case PROP_DESCRIPTION:
        xaccAccountSetDescription(account, g_value_get_string(value));
        break;
    case PROP_COLOR:
        xaccAccountSetColor(account, g_value_get_string(value));
        break;
    case PROP_NOTES:
        xaccAccountSetNotes(account, g_value_get_string(value));
        break;
    case PROP_TYPE:
        // NEED TO BE CONVERTED TO A G_TYPE_ENUM
        xaccAccountSetType(account, static_cast<GNCAccountType>(g_value_get_int(value)));
        break;
    case PROP_COMMODITY:
        xaccAccountSetCommodity(account, static_cast<gnc_commodity*>(g_value_get_object(value)));
        break;
    case PROP_COMMODITY_SCU:
        xaccAccountSetCommoditySCU(account, g_value_get_int(value));
        break;
    case PROP_NON_STD_SCU:
        xaccAccountSetNonStdSCU(account, g_value_get_boolean(value));
        break;
    case PROP_SORT_DIRTY:
        gnc_account_set_sort_dirty(account);
        break;
    case PROP_BALANCE_DIRTY:
        gnc_account_set_balance_dirty(account);
        break;
    case PROP_START_BALANCE:
        number = static_cast<gnc_numeric*>(g_value_get_boxed(value));
        gnc_account_set_start_balance(account, *number);
        break;
    case PROP_START_CLEARED_BALANCE:
        number = static_cast<gnc_numeric*>(g_value_get_boxed(value));
        gnc_account_set_start_cleared_balance(account, *number);
        break;
    case PROP_START_RECONCILED_BALANCE:
        number = static_cast<gnc_numeric*>(g_value_get_boxed(value));
        gnc_account_set_start_reconciled_balance(account, *number);
        break;
    case PROP_POLICY:
        gnc_account_set_policy(account, static_cast<GNCPolicy*>(g_value_get_pointer(value)));
        break;
    case PROP_MARK:
        xaccAccountSetMark(account, g_value_get_int(value));
        break;
    case PROP_TAX_RELATED:
        xaccAccountSetTaxRelated(account, g_value_get_boolean(value));
        break;
    case PROP_TAX_CODE:
        xaccAccountSetTaxUSCode(account, g_value_get_string(value));
        break;
    case PROP_TAX_SOURCE:
        xaccAccountSetTaxUSPayerNameSource(account,
                                           g_value_get_string(value));
        break;
    case PROP_TAX_COPY_NUMBER:
        xaccAccountSetTaxUSCopyNumber(account,
                                      g_value_get_int64(value));
        break;
    case PROP_HIDDEN:
        xaccAccountSetHidden(account, g_value_get_boolean(value));
        break;
    case PROP_AUTO_INTEREST:
        xaccAccountSetAutoInterest (account, g_value_get_boolean (value));
        break;
    case PROP_IS_OPENING_BALANCE:
        xaccAccountSetIsOpeningBalance (account, g_value_get_boolean (value));
        break;
    case PROP_PLACEHOLDER:
        xaccAccountSetPlaceholder(account, g_value_get_boolean(value));
        break;
    case PROP_FILTER:
        xaccAccountSetFilter(account, g_value_get_string(value));
        break;
    case PROP_SORT_ORDER:
        xaccAccountSetSortOrder(account, g_value_get_string(value));
        break;
    case PROP_SORT_REVERSED:
        xaccAccountSetSortReversed(account, g_value_get_boolean(value));
        break;
    case PROP_LOT_NEXT_ID:
        qof_instance_set_path_kvp (QOF_INSTANCE (account), value, {KEY_LOT_MGMT, "next-id"});
        break;
    case PROP_ONLINE_ACCOUNT:
        qof_instance_set_path_kvp (QOF_INSTANCE (account), value, {KEY_ONLINE_ID});
        break;
    case PROP_IMP_APPEND_TEXT:
        xaccAccountSetAppendText(account, g_value_get_boolean(value));
        break;
    case PROP_OFX_INCOME_ACCOUNT:
        qof_instance_set_path_kvp (QOF_INSTANCE (account), value, {KEY_ASSOC_INCOME_ACCOUNT});
        break;
    case PROP_AB_ACCOUNT_ID:
        qof_instance_set_path_kvp (QOF_INSTANCE (account), value, {AB_KEY, AB_ACCOUNT_ID});
        break;
    case PROP_AB_ACCOUNT_UID:
        qof_instance_set_path_kvp (QOF_INSTANCE (account), value, {AB_KEY, AB_ACCOUNT_UID});
        break;
    case PROP_AB_BANK_CODE:
        qof_instance_set_path_kvp (QOF_INSTANCE (account), value, {AB_KEY, AB_BANK_CODE});
        break;
    case PROP_AB_TRANS_RETRIEVAL:
        qof_instance_set_path_kvp (QOF_INSTANCE (account), value, {AB_KEY, AB_TRANS_RETRIEVAL});
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static void
gnc_account_class_init (AccountClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS (klass);

    gobject_class->dispose = gnc_account_dispose;
    gobject_class->finalize = gnc_account_finalize;
    gobject_class->set_property = gnc_account_set_property;
    gobject_class->get_property = gnc_account_get_property;

    g_object_class_install_property
    (gobject_class,
     PROP_NAME,
     g_param_spec_string ("name",
                          "Account Name",
                          "The accountName is an arbitrary string "
                          "assigned by the user.  It is intended to "
                          "a short, 5 to 30 character long string "
                          "that is displayed by the GUI as the "
                          "account mnemonic.  Account names may be "
                          "repeated. but no two accounts that share "
                          "a parent may have the same name.",
                          nullptr,
                          static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_FULL_NAME,
     g_param_spec_string ("fullname",
                          "Full Account Name",
                          "The name of the account concatenated with "
                          "all its parent account names to indicate "
                          "a unique account.",
                          nullptr,
                          static_cast<GParamFlags>(G_PARAM_READABLE)));

    g_object_class_install_property
    (gobject_class,
     PROP_CODE,
     g_param_spec_string ("code",
                          "Account Code",
                          "The account code is an arbitrary string "
                          "assigned by the user. It is intended to "
                          "be reporting code that is a synonym for "
                          "the accountName.",
                          nullptr,
                          static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_DESCRIPTION,
     g_param_spec_string ("description",
                          "Account Description",
                          "The account description is an arbitrary "
                          "string assigned by the user. It is intended "
                          "to be a longer, 1-5 sentence description of "
                          "what this account is all about.",
                          nullptr,
                          static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_COLOR,
     g_param_spec_string ("color",
                          "Account Color",
                          "The account color is a color string assigned "
                          "by the user. It is intended to highlight the "
                          "account based on the users wishes.",
                          nullptr,
                          static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_NOTES,
     g_param_spec_string ("notes",
                          "Account Notes",
                          "The account notes is an arbitrary provided "
                          "for the user to attach any other text that "
                          "they would like to associate with the account.",
                          nullptr,
                          static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_TYPE,
     g_param_spec_int ("type",
                       "Account Type",
                       "The account type, picked from the enumerated list "
                       "that includes ACCT_TYPE_BANK, ACCT_TYPE_STOCK, "
                       "ACCT_TYPE_CREDIT, ACCT_TYPE_INCOME, etc.",
                       ACCT_TYPE_NONE,
                       NUM_ACCOUNT_TYPES - 1,
                       ACCT_TYPE_BANK,
                       static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_COMMODITY,
     g_param_spec_object ("commodity",
                          "Commodity",
                          "The commodity field denotes the kind of "
                          "'stuff' stored  in this account, whether "
                          "it is USD, gold, stock, etc.",
                          GNC_TYPE_COMMODITY,
                          static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_COMMODITY_SCU,
     g_param_spec_int ("commodity-scu",
                       "Commodity SCU",
                       "The smallest fraction of the commodity that is "
                       "tracked.  This number is used as the denominator "
                       "value in 1/x, so a value of 100 says that the "
                       "commodity can be divided into hundredths.  E.G."
                       "1 USD can be divided into 100 cents.",
                       0,
                       G_MAXINT32,
                       GNC_COMMODITY_MAX_FRACTION,
                       static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_NON_STD_SCU,
     g_param_spec_boolean ("non-std-scu",
                           "Non-std SCU",
                           "TRUE if the account SCU doesn't match "
                           "the commodity SCU.  This indicates a case "
                           "where the two were accidentally set to "
                           "mismatched values in older versions of "
                           "GnuCash.",
                           FALSE,
                           static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_SORT_DIRTY,
     g_param_spec_boolean("sort-dirty",
                          "Sort Dirty",
                          "TRUE if the splits in the account needs to be "
                          "resorted.  This flag is set by the accounts "
                          "code for certain internal modifications, or "
                          "when external code calls the engine to say a "
                          "split has been modified in a way that may "
                          "affect the sort order of the account. Note: "
                          "This value can only be set to TRUE.",
                          FALSE,
                          static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_BALANCE_DIRTY,
     g_param_spec_boolean("balance-dirty",
                          "Balance Dirty",
                          "TRUE if the running balances in the account "
                          "needs to be recalculated.  This flag is set "
                          "by the accounts code for certain internal "
                          "modifications, or when external code calls "
                          "the engine to say a split has been modified. "
                          "Note: This value can only be set to TRUE.",
                          FALSE,
                          static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_START_BALANCE,
     g_param_spec_boxed("start-balance",
                        "Starting Balance",
                        "The starting balance for the account.  This "
                        "parameter is intended for use with backends that "
                        "do not return the complete list of splits for an "
                        "account, but rather return a partial list.  In "
                        "such a case, the backend will typically return "
                        "all of the splits after some certain date, and "
                        "the 'starting balance' will represent the "
                        "summation of the splits up to that date.",
                        GNC_TYPE_NUMERIC,
                        static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_START_NOCLOSING_BALANCE,
     g_param_spec_boxed("start-noclosing-balance",
                        "Starting No-closing Balance",
                        "The starting balance for the account, ignoring closing."
                        "This parameter is intended for use with backends "
                        "that do not return the complete list of splits "
                        "for an account, but rather return a partial "
                        "list.  In such a case, the backend will "
                        "typically return all of the splits after "
                        "some certain date, and the 'starting noclosing "
                        "balance' will represent the summation of the "
                        "splits up to that date, ignoring closing splits.",
                        GNC_TYPE_NUMERIC,
                        static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_START_CLEARED_BALANCE,
     g_param_spec_boxed("start-cleared-balance",
                        "Starting Cleared Balance",
                        "The starting cleared balance for the account.  "
                        "This parameter is intended for use with backends "
                        "that do not return the complete list of splits "
                        "for an account, but rather return a partial "
                        "list.  In such a case, the backend will "
                        "typically return all of the splits after "
                        "some certain date, and the 'starting cleared "
                        "balance' will represent the summation of the "
                        "splits up to that date.",
                        GNC_TYPE_NUMERIC,
                        static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_START_RECONCILED_BALANCE,
     g_param_spec_boxed("start-reconciled-balance",
                        "Starting Reconciled Balance",
                        "The starting reconciled balance for the "
                        "account.  This parameter is intended for use "
                        "with backends that do not return the complete "
                        "list of splits for an account, but rather return "
                        "a partial list.  In such a case, the backend "
                        "will typically return all of the splits after "
                        "some certain date, and the 'starting reconciled "
                        "balance' will represent the summation of the "
                        "splits up to that date.",
                        GNC_TYPE_NUMERIC,
                        static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_END_BALANCE,
     g_param_spec_boxed("end-balance",
                        "Ending Account Balance",
                        "This is the current ending balance for the "
                        "account.  It is computed from the sum of the "
                        "starting balance and all splits in the account.",
                        GNC_TYPE_NUMERIC,
                        G_PARAM_READABLE));

    g_object_class_install_property
    (gobject_class,
     PROP_END_NOCLOSING_BALANCE,
     g_param_spec_boxed("end-noclosing-balance",
                        "Ending Account Noclosing Balance",
                        "This is the current ending no-closing balance for "
                        "the account.  It is computed from the sum of the "
                        "starting balance and all cleared splits in the "
                        "account.",
                        GNC_TYPE_NUMERIC,
                        G_PARAM_READABLE));

    g_object_class_install_property
    (gobject_class,
     PROP_END_CLEARED_BALANCE,
     g_param_spec_boxed("end-cleared-balance",
                        "Ending Account Cleared Balance",
                        "This is the current ending cleared balance for "
                        "the account.  It is computed from the sum of the "
                        "starting balance and all cleared splits in the "
                        "account.",
                        GNC_TYPE_NUMERIC,
                        G_PARAM_READABLE));

    g_object_class_install_property
    (gobject_class,
     PROP_END_RECONCILED_BALANCE,
     g_param_spec_boxed("end-reconciled-balance",
                        "Ending Account Reconciled Balance",
                        "This is the current ending reconciled balance "
                        "for the account.  It is computed from the sum of "
                        "the starting balance and all reconciled splits "
                        "in the account.",
                        GNC_TYPE_NUMERIC,
                        static_cast<GParamFlags>(G_PARAM_READABLE)));

    g_object_class_install_property
    (gobject_class,
     PROP_POLICY,
     g_param_spec_pointer ("policy",
                           "Policy",
                           "The account lots policy.",
                           static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_MARK,
     g_param_spec_int ("acct-mark",
                       "Account Mark",
                       "Ipsum Lorem",
                       0,
                       G_MAXINT16,
                       0,
                       static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_TAX_RELATED,
     g_param_spec_boolean ("tax-related",
                           "Tax Related",
                           "Whether the account maps to an entry on an "
                           "income tax document.",
                           FALSE,
                           static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_IS_OPENING_BALANCE,
     g_param_spec_boolean ("opening-balance",
                           "Opening Balance",
                           "Whether the account holds opening balances",
                           FALSE,
                           static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_TAX_CODE,
     g_param_spec_string ("tax-code",
                          "Tax Code",
                          "This is the code for mapping an account to a "
                          "specific entry on a taxable document.  In the "
                          "United States it is used to transfer totals "
                          "into tax preparation software.",
                          nullptr,
                          static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_TAX_SOURCE,
     g_param_spec_string ("tax-source",
                          "Tax Source",
                          "This specifies where exported name comes from.",
                          nullptr,
                          static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_TAX_COPY_NUMBER,
     g_param_spec_int64 ("tax-copy-number",
                         "Tax Copy Number",
                         "This specifies the copy number of the tax "
                         "form/schedule.",
                         (gint64)1,
                         G_MAXINT64,
                         (gint64)1,
                         static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_HIDDEN,
     g_param_spec_boolean ("hidden",
                           "Hidden",
                           "Whether the account should be hidden in the  "
                           "account tree.",
                           FALSE,
                           static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_AUTO_INTEREST,
     g_param_spec_boolean ("auto-interest-transfer",
                           "Auto Interest",
                           "Whether an interest transfer should be automatically  "
                           "added before reconcile.",
                           FALSE,
                           static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_PLACEHOLDER,
     g_param_spec_boolean ("placeholder",
                           "Placeholder",
                           "Whether the account is a placeholder account which does not "
                           "allow transactions to be created, edited or deleted.",
                           FALSE,
                           static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_FILTER,
     g_param_spec_string ("filter",
                          "Account Filter",
                          "The account filter is a value saved to allow "
                          "filters to be recalled.",
                          nullptr,
                          static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_SORT_ORDER,
     g_param_spec_string ("sort-order",
                          "Account Sort Order",
                          "The account sort order is a value saved to allow "
                          "the sort order to be recalled.",
                          nullptr,
                          static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_SORT_REVERSED,
     g_param_spec_boolean ("sort-reversed",
                          "Account Sort Reversed",
                          "Parameter to store whether the sort order is reversed or not.",
                          FALSE,
                          static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_LOT_NEXT_ID,
     g_param_spec_int64 ("lot-next-id",
                         "Lot Next ID",
                         "Tracks the next id to use in gnc_lot_make_default.",
                         (gint64)1,
                         G_MAXINT64,
                         (gint64)1,
                         static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_ONLINE_ACCOUNT,
     g_param_spec_string ("online-id",
                          "Online Account ID",
                          "The online account which corresponds to this "
                          "account for OFX import",
                          nullptr,
                          static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_IMP_APPEND_TEXT,
     g_param_spec_boolean ("import-append-text",
                           "Import Append Text",
                           "Saved state of Append checkbox for setting initial "
                           "value next time this account is imported.",
                           FALSE,
                           static_cast<GParamFlags>(G_PARAM_READWRITE)));

     g_object_class_install_property(
       gobject_class,
       PROP_OFX_INCOME_ACCOUNT,
        g_param_spec_boxed("ofx-income-account",
                           "Associated income account",
                           "Used by the OFX importer.",
                           GNC_TYPE_GUID,
                           static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_AB_ACCOUNT_ID,
     g_param_spec_string ("ab-account-id",
                          "AQBanking Account ID",
                          "The AqBanking account which corresponds to this "
                          "account for AQBanking import",
                          nullptr,
                          static_cast<GParamFlags>(G_PARAM_READWRITE)));
    g_object_class_install_property
    (gobject_class,
     PROP_AB_BANK_CODE,
     g_param_spec_string ("ab-bank-code",
                          "AQBanking Bank Code",
                          "The online account which corresponds to this "
                          "account for AQBanking import",
                          nullptr,
                          static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_AB_ACCOUNT_UID,
     g_param_spec_int64 ("ab-account-uid",
                         "AQBanking Account UID",
                         "Tracks the next id to use in gnc_lot_make_default.",
                         (gint64)1,
                         G_MAXINT64,
                         (gint64)1,
                         static_cast<GParamFlags>(G_PARAM_READWRITE)));

    g_object_class_install_property
    (gobject_class,
     PROP_AB_TRANS_RETRIEVAL,
     g_param_spec_boxed("ab-trans-retrieval",
                        "AQBanking Last Transaction Retrieval",
                        "The time of the last transaction retrieval for this "
                        "account.",
                        GNC_TYPE_TIME64,
                        static_cast<GParamFlags>(G_PARAM_READWRITE)));

}

static void
xaccInitAccount (Account * acc, QofBook *book)
{
    ENTER ("book=%p\n", book);
    qof_instance_init_data (&acc->inst, GNC_ID_ACCOUNT, book);

    LEAVE ("account=%p\n", acc);
}

/********************************************************************\
\********************************************************************/

void
gnc_account_foreach_split (const Account *acc, std::function<void(Split*)> func,
                           bool reverse)
{
    if (!GNC_IS_ACCOUNT (acc))
        return;

    auto& splits{GET_PRIVATE(acc)->splits};
    if (reverse)
        std::for_each(splits.rbegin(), splits.rend(), func);
    else
        std::for_each(splits.begin(), splits.end(), func);
}

void
gnc_account_foreach_split_until_date (const Account *acc, time64 end_date,
                                      std::function<void(Split*)> f)
{
    if (!GNC_IS_ACCOUNT (acc))
        return;

    auto after_date = [](time64 end_date, auto s) -> bool
    { return (xaccTransGetDate (xaccSplitGetParent (s)) > end_date); };

    auto& splits{GET_PRIVATE(acc)->splits};
    auto after_date_iter = std::upper_bound (splits.begin(), splits.end(), end_date, after_date);
    std::for_each (splits.begin(), after_date_iter, f);
}


Split*
gnc_account_find_split (const Account *acc, std::function<bool(const Split*)> predicate,
                        bool reverse)
{
    if (!GNC_IS_ACCOUNT (acc))
        return nullptr;

    const auto& splits{GET_PRIVATE(acc)->splits};
    if (reverse)
    {
        auto latest = std::find_if(splits.rbegin(), splits.rend(), predicate);
        return (latest == splits.rend()) ? nullptr : *latest;
    }
    else
    {
        auto earliest = std::find_if(splits.begin(), splits.end(), predicate);
        return (earliest == splits.end()) ? nullptr : *earliest;
    }
}

/********************************************************************\
\********************************************************************/

QofBook *
gnc_account_get_book(const Account *account)
{
    if (!account) return nullptr;
    return qof_instance_get_book(QOF_INSTANCE(account));
}

/********************************************************************\
\********************************************************************/

static Account *
gnc_coll_get_root_account (QofCollection *col)
{
    if (!col) return nullptr;
    return static_cast<Account*>(qof_collection_get_data (col));
}

static void
gnc_coll_set_root_account (QofCollection *col, Account *root)
{
    AccountPrivate *rpriv;
    Account *old_root;
    if (!col) return;

    old_root = gnc_coll_get_root_account (col);
    if (old_root == root) return;

    /* If the new root is already linked into the tree somewhere, then
     * remove it from its current position before adding it at the
     * top. */
    rpriv = GET_PRIVATE(root);
    if (rpriv->parent)
    {
        xaccAccountBeginEdit(root);
        gnc_account_remove_child(rpriv->parent, root);
        xaccAccountCommitEdit(root);
    }

    qof_collection_set_data (col, root);

    if (old_root)
    {
        xaccAccountBeginEdit (old_root);
        xaccAccountDestroy (old_root);
    }
}

Account *
gnc_book_get_root_account (QofBook *book)
{
    QofCollection *col;
    Account *root;

    if (!book) return nullptr;
    col = qof_book_get_collection (book, GNC_ID_ROOT_ACCOUNT);
    root = gnc_coll_get_root_account (col);
    if (root == nullptr && !qof_book_shutting_down(book))
        root = gnc_account_create_root(book);
    return root;
}

void
gnc_book_set_root_account (QofBook *book, Account *root)
{
    QofCollection *col;
    if (!book) return;

    if (root && gnc_account_get_book(root) != book)
    {
        PERR ("cannot mix and match books freely!");
        return;
    }

    col = qof_book_get_collection (book, GNC_ID_ROOT_ACCOUNT);
    gnc_coll_set_root_account (col, root);
}

/********************************************************************\
\********************************************************************/

Account *
xaccMallocAccount (QofBook *book)
{
    Account *acc;

    g_return_val_if_fail (book, nullptr);

    acc = static_cast<Account*>(g_object_new (GNC_TYPE_ACCOUNT, nullptr));
    xaccInitAccount (acc, book);
    qof_event_gen (&acc->inst, QOF_EVENT_CREATE, nullptr);

    return acc;
}

Account *
gnc_account_create_root (QofBook *book)
{
    Account *root;
    AccountPrivate *rpriv;

    root = xaccMallocAccount(book);
    rpriv = GET_PRIVATE(root);
    xaccAccountBeginEdit(root);
    rpriv->type = ACCT_TYPE_ROOT;
    rpriv->accountName = qof_string_cache_replace(rpriv->accountName, "Root Account");
    mark_account (root);
    xaccAccountCommitEdit(root);
    gnc_book_set_root_account(book, root);
    return root;
}

Account *
xaccCloneAccount(const Account *from, QofBook *book)
{
    Account *ret;
    AccountPrivate *from_priv, *priv;

    g_return_val_if_fail(GNC_IS_ACCOUNT(from), nullptr);
    g_return_val_if_fail(QOF_IS_BOOK(book), nullptr);

    ENTER (" ");
    ret = static_cast<Account*>(g_object_new (GNC_TYPE_ACCOUNT, nullptr));
    g_return_val_if_fail (ret, nullptr);

    from_priv = GET_PRIVATE(from);
    priv = GET_PRIVATE(ret);
    xaccInitAccount (ret, book);

    /* Do not Begin/CommitEdit() here; give the caller
     * a chance to fix things up, and let them do it.
     * Also let caller issue the generate_event (EVENT_CREATE) */
    priv->type = from_priv->type;

    priv->accountName = qof_string_cache_replace(priv->accountName, from_priv->accountName);
    priv->accountCode = qof_string_cache_replace(priv->accountCode, from_priv->accountCode);
    priv->description = qof_string_cache_replace(priv->description, from_priv->description);

    qof_instance_copy_kvp (QOF_INSTANCE (ret), QOF_INSTANCE (from));

    /* The new book should contain a commodity that matches
     * the one in the old book. Find it, use it. */
    priv->commodity = gnc_commodity_obtain_twin(from_priv->commodity, book);
    gnc_commodity_increment_usage_count(priv->commodity);

    priv->commodity_scu = from_priv->commodity_scu;
    priv->non_standard_scu = from_priv->non_standard_scu;

    qof_instance_set_dirty(&ret->inst);
    LEAVE (" ");
    return ret;
}

/********************************************************************\
\********************************************************************/

static void
xaccFreeOneChildAccount (Account *acc)
{
    /* FIXME: this code is kind of hacky.  actually, all this code
     * seems to assume that the account edit levels are all 1. */
    if (qof_instance_get_editlevel(acc) == 0)
        xaccAccountBeginEdit(acc);
    xaccAccountDestroy(acc);
}

static void
xaccFreeAccountChildren (Account *acc)
{
    auto priv{GET_PRIVATE(acc)};
    /* Copy the list since it will be modified */
    auto children = priv->children;
    std::for_each (children.begin(), children.end(), xaccFreeOneChildAccount);

    /* The foreach should have removed all the children already. */
    priv->children.clear();
}

/* The xaccFreeAccount() routine releases memory associated with the
 * account.  It should never be called directly from user code;
 * instead, the xaccAccountDestroy() routine should be used (because
 * xaccAccountDestroy() has the correct commit semantics). */
static void
xaccFreeAccount (Account *acc)
{
    AccountPrivate *priv;
    GList *lp;

    g_return_if_fail(GNC_IS_ACCOUNT(acc));

    priv = GET_PRIVATE(acc);
    qof_event_gen (&acc->inst, QOF_EVENT_DESTROY, nullptr);

    /* Otherwise the lists below get munged while we're iterating
     * them, possibly crashing.
     */
    if (!qof_instance_get_destroying (acc))
            qof_instance_set_destroying(acc, TRUE);

    if (!priv->children.empty())
    {
        PERR (" instead of calling xaccFreeAccount(), please call\n"
              " xaccAccountBeginEdit(); xaccAccountDestroy();\n");

        /* First, recursively free children, also frees list */
        xaccFreeAccountChildren(acc);
    }

    /* remove lots -- although these should be gone by now. */
    if (priv->lots)
    {
        PERR (" instead of calling xaccFreeAccount(), please call\n"
              " xaccAccountBeginEdit(); xaccAccountDestroy();\n");

        for (lp = priv->lots; lp; lp = lp->next)
        {
            GNCLot *lot = static_cast<GNCLot*>(lp->data);
            gnc_lot_destroy (lot);
        }
        g_list_free (priv->lots);
        priv->lots = nullptr;
    }

    /* Next, clean up the splits */
    /* NB there shouldn't be any splits by now ... they should
     * have been all been freed by CommitEdit().  We can remove this
     * check once we know the warning isn't occurring any more. */
    if (!priv->splits.empty())
    {
        PERR (" instead of calling xaccFreeAccount(), please call\n"
              " xaccAccountBeginEdit(); xaccAccountDestroy();\n");

        qof_instance_reset_editlevel(acc);

        for (auto s : priv->splits)
        {
            g_assert(xaccSplitGetAccount(s) == acc);
            xaccSplitDestroy (s);
        }
/* Nothing here (or in xaccAccountCommitEdit) nullptrs priv->splits, so this asserts every time.
        g_assert(priv->splits == nullptr);
*/
    }

    qof_string_cache_remove(priv->accountName);
    qof_string_cache_remove(priv->accountCode);
    qof_string_cache_remove(priv->description);
    priv->accountName = priv->accountCode = priv->description = nullptr;

    /* zero out values, just in case stray
     * pointers are pointing here. */

    priv->last_num = nullptr;
    priv->tax_us_code = nullptr;
    priv->tax_us_pns = nullptr;
    priv->color = nullptr;
    priv->sort_order = nullptr;
    priv->notes = nullptr;
    priv->filter = nullptr;

    priv->parent = nullptr;

    priv->balance  = gnc_numeric_zero();
    priv->noclosing_balance = gnc_numeric_zero();
    priv->cleared_balance = gnc_numeric_zero();
    priv->reconciled_balance = gnc_numeric_zero();

    priv->type = ACCT_TYPE_NONE;
    gnc_commodity_decrement_usage_count(priv->commodity);
    priv->commodity = nullptr;

    priv->balance_dirty = FALSE;
    priv->sort_dirty = FALSE;
    priv->splits.~SplitsVec();
    priv->children.~AccountVec();
    g_hash_table_destroy (priv->splits_hash);

    /* qof_instance_release (&acc->inst); */
    g_object_unref(acc);
}

/********************************************************************\
 * transactional routines
\********************************************************************/

void
xaccAccountBeginEdit (Account *acc)
{
    g_return_if_fail(acc);
    qof_begin_edit(&acc->inst);
}

static void on_done(QofInstance *inst)
{
    /* old event style */
    qof_event_gen (inst, QOF_EVENT_MODIFY, nullptr);
}

static void on_err (QofInstance *inst, QofBackendError errcode)
{
    PERR("commit error: %d", errcode);
    gnc_engine_signal_commit_error( errcode );
}

static void acc_free (QofInstance *inst)
{
    AccountPrivate *priv;
    Account *acc = (Account *) inst;

    priv = GET_PRIVATE(acc);
    if (priv->parent)
        gnc_account_remove_child(priv->parent, acc);
    xaccFreeAccount(acc);
}

static void
destroy_pending_splits_for_account(QofInstance *ent, gpointer acc)
{
    Transaction *trans = (Transaction *) ent;
    Split *split;

    if (xaccTransIsOpen(trans))
        while ((split = xaccTransFindSplitByAccount(trans, static_cast<Account*>(acc))))
            xaccSplitDestroy(split);
}

void
xaccAccountCommitEdit (Account *acc)
{
    AccountPrivate *priv;
    QofBook *book;

    g_return_if_fail(acc);
    if (!qof_commit_edit(&acc->inst)) return;

    /* If marked for deletion, get rid of subaccounts first,
     * and then the splits ... */
    priv = GET_PRIVATE(acc);
    if (qof_instance_get_destroying(acc))
    {
        QofCollection *col;

        qof_instance_increase_editlevel(acc);

        /* First, recursively free children */
        xaccFreeAccountChildren(acc);

        PINFO ("freeing splits for account %p (%s)",
               acc, priv->accountName ? priv->accountName : "(null)");

        book = qof_instance_get_book(acc);

        /* If book is shutting down, just clear the split list.  The splits
           themselves will be destroyed by the transaction code */
        if (!qof_book_shutting_down(book))
        {
            // We need to delete in reverse order so that the vector's iterators aren't invalidated.
            for_each(priv->splits.rbegin(), priv->splits.rend(), [](Split *s) {
                xaccSplitDestroy (s); });
        }
        else
        {
            priv->splits.clear();
            g_hash_table_remove_all (priv->splits_hash);
        }

        /* It turns out there's a case where this assertion does not hold:
           When the user tries to delete an Imbalance account, while also
           deleting all the splits in it.  The splits will just get
           recreated and put right back into the same account!

           g_assert(priv->splits == nullptr || qof_book_shutting_down(acc->inst.book));
        */

        if (!qof_book_shutting_down(book))
        {
            col = qof_book_get_collection(book, GNC_ID_TRANS);
            qof_collection_foreach(col, destroy_pending_splits_for_account, acc);

            /* the lots should be empty by now */
            for (auto lp = priv->lots; lp; lp = lp->next)
            {
                GNCLot *lot = static_cast<GNCLot*>(lp->data);
                gnc_lot_destroy (lot);
            }
        }
        g_list_free(priv->lots);
        priv->lots = nullptr;

        qof_instance_set_dirty(&acc->inst);
        qof_instance_decrease_editlevel(acc);
    }
    else
    {
        xaccAccountBringUpToDate(acc);
    }

    qof_commit_edit_part2(&acc->inst, on_err, on_done, acc_free);
}

void
xaccAccountDestroy (Account *acc)
{
    g_return_if_fail(GNC_IS_ACCOUNT(acc));

    qof_instance_set_destroying(acc, TRUE);

    xaccAccountCommitEdit (acc);
}

/********************************************************************\
\********************************************************************/

static gboolean
xaccAcctChildrenEqual(const AccountVec& na,
                      const AccountVec& nb,
                      gboolean check_guids)
{
    if (na.size() != nb.size())
    {
        PINFO ("Accounts have different numbers of children");
        return (FALSE);
    }

    for (auto aa : na)
    {
        auto it_b = std::find_if (nb.begin(), nb.end(), [aa](auto ab) -> bool
        {
            if (!aa) return (!ab);
            if (!ab) return false;
            auto code_a{GET_PRIVATE(aa)->accountCode};
            auto code_b{GET_PRIVATE(ab)->accountCode};
            if ((code_a && *code_a) || (code_b && *code_b)) return !g_strcmp0 (code_a, code_b);
            return !g_strcmp0 (GET_PRIVATE(aa)->accountName, GET_PRIVATE(ab)->accountName);
        });

        if (it_b == nb.end())
        {
            PINFO ("Unable to find matching child account.");
            return FALSE;
        }
        else if (auto ab = *it_b; !xaccAccountEqual(aa, ab, check_guids))
        {
            char sa[GUID_ENCODING_LENGTH + 1];
            char sb[GUID_ENCODING_LENGTH + 1];

            guid_to_string_buff (xaccAccountGetGUID (aa), sa);
            guid_to_string_buff (xaccAccountGetGUID (ab), sb);

            PWARN ("accounts %s and %s differ", sa, sb);

            return(FALSE);
        }
    }

    return(TRUE);
}

gboolean
xaccAccountEqual(const Account *aa, const Account *ab, gboolean check_guids)
{
    AccountPrivate *priv_aa, *priv_ab;

    if (!aa && !ab) return TRUE;

    g_return_val_if_fail(GNC_IS_ACCOUNT(aa), FALSE);
    g_return_val_if_fail(GNC_IS_ACCOUNT(ab), FALSE);

    priv_aa = GET_PRIVATE(aa);
    priv_ab = GET_PRIVATE(ab);
    if (priv_aa->type != priv_ab->type)
    {
        PWARN ("types differ: %d vs %d", priv_aa->type, priv_ab->type);
        return FALSE;
    }

    if (g_strcmp0(priv_aa->accountName, priv_ab->accountName) != 0)
    {
        PWARN ("names differ: %s vs %s", priv_aa->accountName, priv_ab->accountName);
        return FALSE;
    }

    if (g_strcmp0(priv_aa->accountCode, priv_ab->accountCode) != 0)
    {
        PWARN ("codes differ: %s vs %s", priv_aa->accountCode, priv_ab->accountCode);
        return FALSE;
    }

    if (g_strcmp0(priv_aa->description, priv_ab->description) != 0)
    {
        PWARN ("descriptions differ: %s vs %s", priv_aa->description, priv_ab->description);
        return FALSE;
    }

    if (!gnc_commodity_equal(priv_aa->commodity, priv_ab->commodity))
    {
        PWARN ("commodities differ");
        return FALSE;
    }

    if (check_guids)
    {
        if (qof_instance_guid_compare(aa, ab) != 0)
        {
            PWARN ("GUIDs differ");
            return FALSE;
        }
    }

    if (qof_instance_compare_kvp (QOF_INSTANCE (aa), QOF_INSTANCE (ab)) != 0)
    {
        char *frame_a;
        char *frame_b;

        frame_a = qof_instance_kvp_as_string (QOF_INSTANCE (aa));
        frame_b = qof_instance_kvp_as_string (QOF_INSTANCE (ab));

        PWARN ("kvp frames differ:\n%s\n\nvs\n\n%s", frame_a, frame_b);

        g_free (frame_a);
        g_free (frame_b);

        return FALSE;
    }

    if (!gnc_numeric_equal(priv_aa->starting_balance, priv_ab->starting_balance))
    {
        char *str_a;
        char *str_b;

        str_a = gnc_numeric_to_string(priv_aa->starting_balance);
        str_b = gnc_numeric_to_string(priv_ab->starting_balance);

        PWARN ("starting balances differ: %s vs %s", str_a, str_b);

        g_free (str_a);
        g_free (str_b);

        return FALSE;
    }

    if (!gnc_numeric_equal(priv_aa->starting_noclosing_balance,
                           priv_ab->starting_noclosing_balance))
    {
        char *str_a;
        char *str_b;

        str_a = gnc_numeric_to_string(priv_aa->starting_noclosing_balance);
        str_b = gnc_numeric_to_string(priv_ab->starting_noclosing_balance);

        PWARN ("starting noclosing balances differ: %s vs %s", str_a, str_b);

        g_free (str_a);
        g_free (str_b);

        return FALSE;
    }
    if (!gnc_numeric_equal(priv_aa->starting_cleared_balance,
                           priv_ab->starting_cleared_balance))
    {
        char *str_a;
        char *str_b;

        str_a = gnc_numeric_to_string(priv_aa->starting_cleared_balance);
        str_b = gnc_numeric_to_string(priv_ab->starting_cleared_balance);

        PWARN ("starting cleared balances differ: %s vs %s", str_a, str_b);

        g_free (str_a);
        g_free (str_b);

        return FALSE;
    }

    if (!gnc_numeric_equal(priv_aa->starting_reconciled_balance,
                           priv_ab->starting_reconciled_balance))
    {
        char *str_a;
        char *str_b;

        str_a = gnc_numeric_to_string(priv_aa->starting_reconciled_balance);
        str_b = gnc_numeric_to_string(priv_ab->starting_reconciled_balance);

        PWARN ("starting reconciled balances differ: %s vs %s", str_a, str_b);

        g_free (str_a);
        g_free (str_b);

        return FALSE;
    }

    if (!gnc_numeric_equal(priv_aa->balance, priv_ab->balance))
    {
        char *str_a;
        char *str_b;

        str_a = gnc_numeric_to_string(priv_aa->balance);
        str_b = gnc_numeric_to_string(priv_ab->balance);

        PWARN ("balances differ: %s vs %s", str_a, str_b);

        g_free (str_a);
        g_free (str_b);

        return FALSE;
    }

    if (!gnc_numeric_equal(priv_aa->noclosing_balance, priv_ab->noclosing_balance))
    {
        char *str_a;
        char *str_b;

        str_a = gnc_numeric_to_string(priv_aa->noclosing_balance);
        str_b = gnc_numeric_to_string(priv_ab->noclosing_balance);

        PWARN ("noclosing balances differ: %s vs %s", str_a, str_b);

        g_free (str_a);
        g_free (str_b);

        return FALSE;
    }
    if (!gnc_numeric_equal(priv_aa->cleared_balance, priv_ab->cleared_balance))
    {
        char *str_a;
        char *str_b;

        str_a = gnc_numeric_to_string(priv_aa->cleared_balance);
        str_b = gnc_numeric_to_string(priv_ab->cleared_balance);

        PWARN ("cleared balances differ: %s vs %s", str_a, str_b);

        g_free (str_a);
        g_free (str_b);

        return FALSE;
    }

    if (!gnc_numeric_equal(priv_aa->reconciled_balance, priv_ab->reconciled_balance))
    {
        char *str_a;
        char *str_b;

        str_a = gnc_numeric_to_string(priv_aa->reconciled_balance);
        str_b = gnc_numeric_to_string(priv_ab->reconciled_balance);

        PWARN ("reconciled balances differ: %s vs %s", str_a, str_b);

        g_free (str_a);
        g_free (str_b);

        return FALSE;
    }

    /* no parent; always compare downwards. */

    if (!std::equal (priv_aa->splits.begin(), priv_aa->splits.end(),
                     priv_ab->splits.begin(), priv_ab->splits.end(),
                     [check_guids](auto sa, auto sb)
                     { return xaccSplitEqual(sa, sb, check_guids, true, false); }))
    {
        PWARN ("splits differ");
        return false;
    }

    if (!xaccAcctChildrenEqual(priv_aa->children, priv_ab->children, check_guids))
    {
        PWARN ("children differ");
        return FALSE;
    }

    return(TRUE);
}

/********************************************************************\
\********************************************************************/
void
gnc_account_set_sort_dirty (Account *acc)
{
    AccountPrivate *priv;

    g_return_if_fail(GNC_IS_ACCOUNT(acc));

    if (qof_instance_get_destroying(acc))
        return;

    priv = GET_PRIVATE(acc);
    priv->sort_dirty = TRUE;
}

void
gnc_account_set_balance_dirty (Account *acc)
{
    AccountPrivate *priv;

    g_return_if_fail(GNC_IS_ACCOUNT(acc));

    if (qof_instance_get_destroying(acc))
        return;

    priv = GET_PRIVATE(acc);
    priv->balance_dirty = TRUE;
}

void gnc_account_set_defer_bal_computation (Account *acc, gboolean defer)
{
    AccountPrivate *priv;

    g_return_if_fail (GNC_IS_ACCOUNT (acc));

    if (qof_instance_get_destroying (acc))
        return;

    priv = GET_PRIVATE (acc);
    priv->defer_bal_computation = defer;
}

gboolean gnc_account_get_defer_bal_computation (Account *acc)
{
    AccountPrivate *priv;
    if (!acc)
        return false;
    priv = GET_PRIVATE (acc);
    return priv->defer_bal_computation;
}


/********************************************************************\
\********************************************************************/

static bool split_cmp_less (const Split* a, const Split* b)
{
    return xaccSplitOrder (a, b) < 0;
}

gboolean
gnc_account_insert_split (Account *acc, Split *s)
{
    AccountPrivate *priv;

    g_return_val_if_fail(GNC_IS_ACCOUNT(acc), FALSE);
    g_return_val_if_fail(GNC_IS_SPLIT(s), FALSE);

    priv = GET_PRIVATE(acc);
    if (!g_hash_table_add (priv->splits_hash, s))
        return false;

    priv->splits.push_back (s);

    if (qof_instance_get_editlevel(acc) == 0)
        std::sort (priv->splits.begin(), priv->splits.end(), split_cmp_less);
    else
        priv->sort_dirty = true;

    //FIXME: find better event
    qof_event_gen (&acc->inst, QOF_EVENT_MODIFY, nullptr);
    /* Also send an event based on the account */
    qof_event_gen(&acc->inst, GNC_EVENT_ITEM_ADDED, s);

    priv->balance_dirty = TRUE;
//  DRH: Should the below be added? It is present in the delete path.
//  xaccAccountRecomputeBalance(acc);
    return TRUE;
}

gboolean
gnc_account_remove_split (Account *acc, Split *s)
{
    AccountPrivate *priv;

    g_return_val_if_fail(GNC_IS_ACCOUNT(acc), FALSE);
    g_return_val_if_fail(GNC_IS_SPLIT(s), FALSE);

    priv = GET_PRIVATE(acc);

    if (!g_hash_table_remove (priv->splits_hash, s))
        return false;

    // shortcut pruning the last element. this is the most common
    // remove_split operation during UI or book shutdown.
    if (s == priv->splits.back())
        priv->splits.pop_back();
    else
        priv->splits.erase (std::remove (priv->splits.begin(), priv->splits.end(), s),
                            priv->splits.end());

    //FIXME: find better event type
    qof_event_gen(&acc->inst, QOF_EVENT_MODIFY, nullptr);
    // And send the account-based event, too
    qof_event_gen(&acc->inst, GNC_EVENT_ITEM_REMOVED, s);

    priv->balance_dirty = TRUE;
    xaccAccountRecomputeBalance(acc);
    return TRUE;
}

void
xaccAccountSortSplits (Account *acc, gboolean force)
{
    AccountPrivate *priv;

    g_return_if_fail(GNC_IS_ACCOUNT(acc));

    priv = GET_PRIVATE(acc);
    if (!priv->sort_dirty || (!force && qof_instance_get_editlevel(acc) > 0))
        return;
    std::sort (priv->splits.begin(), priv->splits.end(), split_cmp_less);
    priv->sort_dirty = FALSE;
    priv->balance_dirty = TRUE;
}

static void
xaccAccountBringUpToDate(Account *acc)
{
    if (!acc) return;

    /* if a re-sort happens here, then everything will update, so the
       cost basis and balance calls are no-ops */
    xaccAccountSortSplits(acc, FALSE);
    xaccAccountRecomputeBalance(acc);
}

/********************************************************************\
\********************************************************************/

void
xaccAccountSetGUID (Account *acc, const GncGUID *guid)
{
    g_return_if_fail(GNC_IS_ACCOUNT(acc));
    g_return_if_fail(guid);

    /* XXX this looks fishy and weird to me ... */
    PINFO("acct=%p", acc);
    xaccAccountBeginEdit (acc);
    qof_instance_set_guid (&acc->inst, guid);
    qof_instance_set_dirty(&acc->inst);
    xaccAccountCommitEdit (acc);
}

/********************************************************************\
\********************************************************************/

Account *
xaccAccountLookup (const GncGUID *guid, QofBook *book)
{
    QofCollection *col;
    if (!guid || !book) return nullptr;
    col = qof_book_get_collection (book, GNC_ID_ACCOUNT);
    return (Account *) qof_collection_lookup_entity (col, guid);
}

/********************************************************************\
\********************************************************************/

void
xaccAccountSetMark (Account *acc, short m)
{
    AccountPrivate *priv;

    g_return_if_fail(GNC_IS_ACCOUNT(acc));

    priv = GET_PRIVATE(acc);
    priv->mark = m;
}

void
xaccClearMark (Account *acc, short val)
{
    Account *root;

    g_return_if_fail(GNC_IS_ACCOUNT(acc));

    root = gnc_account_get_root(acc);
    xaccClearMarkDown(root ? root : acc, val);
}

void
xaccClearMarkDown (Account *acc, short val)
{
    AccountPrivate *priv;

    g_return_if_fail(GNC_IS_ACCOUNT(acc));

    priv = GET_PRIVATE(acc);
    priv->mark = val;
    std::for_each (priv->children.begin(), priv->children.end(),
                   [val](auto acc){ xaccClearMarkDown(acc, val); });
}

/********************************************************************\
\********************************************************************/

GNCPolicy *
gnc_account_get_policy (Account *acc)
{
    g_return_val_if_fail(GNC_IS_ACCOUNT(acc), nullptr);

    return GET_PRIVATE(acc)->policy;
}

void
gnc_account_set_policy (Account *acc, GNCPolicy *policy)
{
    AccountPrivate *priv;

    g_return_if_fail(GNC_IS_ACCOUNT(acc));

    priv = GET_PRIVATE(acc);
    priv->policy = policy ? policy : xaccGetFIFOPolicy();
}

/********************************************************************\
\********************************************************************/

void
xaccAccountRemoveLot (Account *acc, GNCLot *lot)
{
    AccountPrivate *priv;

    g_return_if_fail(GNC_IS_ACCOUNT(acc));
    g_return_if_fail(GNC_IS_LOT(lot));

    priv = GET_PRIVATE(acc);
    g_return_if_fail(priv->lots);

    ENTER ("(acc=%p, lot=%p)", acc, lot);
    priv->lots = g_list_remove(priv->lots, lot);
    qof_event_gen (QOF_INSTANCE(lot), QOF_EVENT_REMOVE, nullptr);
    qof_event_gen (&acc->inst, QOF_EVENT_MODIFY, nullptr);
    LEAVE ("(acc=%p, lot=%p)", acc, lot);
}

void
xaccAccountInsertLot (Account *acc, GNCLot *lot)
{
    AccountPrivate *priv, *opriv;
    Account * old_acc = nullptr;
    Account* lot_account;

    /* errors */
    g_return_if_fail(GNC_IS_ACCOUNT(acc));
    g_return_if_fail(GNC_IS_LOT(lot));

    /* optimizations */
    lot_account = gnc_lot_get_account(lot);
    if (lot_account == acc)
        return;

    ENTER ("(acc=%p, lot=%p)", acc, lot);

    /* pull it out of the old account */
    if (lot_account)
    {
        old_acc = lot_account;
        opriv = GET_PRIVATE(old_acc);
        opriv->lots = g_list_remove(opriv->lots, lot);
    }

    priv = GET_PRIVATE(acc);
    priv->lots = g_list_prepend(priv->lots, lot);
    gnc_lot_set_account(lot, acc);

    /* Don't move the splits to the new account.  The caller will do this
     * if appropriate, and doing it here will not work if we are being
     * called from gnc_book_close_period since xaccAccountInsertSplit
     * will try to balance capital gains and things aren't ready for that. */

    qof_event_gen (QOF_INSTANCE(lot), QOF_EVENT_ADD, nullptr);
    qof_event_gen (&acc->inst, QOF_EVENT_MODIFY, nullptr);

    LEAVE ("(acc=%p, lot=%p)", acc, lot);
}

/********************************************************************\
\********************************************************************/
static void
xaccPreSplitMove (Split *split)
{
    xaccTransBeginEdit (xaccSplitGetParent (split));
}

static void
xaccPostSplitMove (Split *split, Account *accto)
{
    Transaction *trans;

    xaccSplitSetAccount(split, accto);
    xaccSplitSetAmount(split, split->amount);
    trans = xaccSplitGetParent (split);
    xaccTransCommitEdit (trans);
}

void
xaccAccountMoveAllSplits (Account *accfrom, Account *accto)
{
    AccountPrivate *from_priv;

    /* errors */
    g_return_if_fail(GNC_IS_ACCOUNT(accfrom));
    g_return_if_fail(GNC_IS_ACCOUNT(accto));

    /* optimizations */
    from_priv = GET_PRIVATE(accfrom);
    if (from_priv->splits.empty() || accfrom == accto)
        return;

    /* check for book mix-up */
    g_return_if_fail (qof_instance_books_equal(accfrom, accto));
    ENTER ("(accfrom=%p, accto=%p)", accfrom, accto);

    xaccAccountBeginEdit(accfrom);
    xaccAccountBeginEdit(accto);
    /* Begin editing both accounts and all transactions in accfrom. */
    std::for_each (from_priv->splits.begin(), from_priv->splits.end(), xaccPreSplitMove);

    /* Concatenate accfrom's lists of splits and lots to accto's lists. */
    //to_priv->splits = g_list_concat(to_priv->splits, from_priv->splits);
    //to_priv->lots = g_list_concat(to_priv->lots, from_priv->lots);

    /* Set appropriate flags. */
    //from_priv->balance_dirty = TRUE;
    //from_priv->sort_dirty = FALSE;
    //to_priv->balance_dirty = TRUE;
    //to_priv->sort_dirty = TRUE;

    /*
     * Change each split's account back pointer to accto.
     * Convert each split's amount to accto's commodity.
     * Commit to editing each transaction.
     */
    auto splits = from_priv->splits;
    std::for_each (splits.begin(), splits.end(), [accto](auto s){ xaccPostSplitMove (s, accto); });

    /* Finally empty accfrom. */
    g_assert(from_priv->splits.empty());
    g_assert(from_priv->lots == nullptr);
    xaccAccountCommitEdit(accfrom);
    xaccAccountCommitEdit(accto);

    LEAVE ("(accfrom=%p, accto=%p)", accfrom, accto);
}


/********************************************************************\
 * xaccAccountRecomputeBalance                                      *
 *   recomputes the partial balances and the current balance for    *
 *   this account.                                                  *
 *                                                                  *
 * The way the computation is done depends on whether the partial   *
 * balances are for a monetary account (bank, cash, etc.) or a      *
 * certificate account (stock portfolio, mutual fund).  For bank    *
 * accounts, the invariant amount is the dollar amount. For share   *
 * accounts, the invariant amount is the number of shares. For      *
 * share accounts, the share price fluctuates, and the current      *
 * value of such an account is the number of shares times the       *
 * current share price.                                             *
 *                                                                  *
 * Part of the complexity of this computation stems from the fact   *
 * xacc uses a double-entry system, meaning that one transaction    *
 * appears in two accounts: one account is debited, and the other   *
 * is credited.  When the transaction represents a sale of shares,  *
 * or a purchase of shares, some care must be taken to compute      *
 * balances correctly.  For a sale of shares, the stock account must*
 * be debited in shares, but the bank account must be credited      *
 * in dollars.  Thus, two different mechanisms must be used to      *
 * compute balances, depending on account type.                     *
 *                                                                  *
 * Args:   account -- the account for which to recompute balances   *
 * Return: void                                                     *
\********************************************************************/

void
xaccAccountRecomputeBalance (Account * acc)
{
    AccountPrivate *priv;
    gnc_numeric  balance;
    gnc_numeric  noclosing_balance;
    gnc_numeric  cleared_balance;
    gnc_numeric  reconciled_balance;

    if (nullptr == acc) return;

    priv = GET_PRIVATE(acc);
    if (qof_instance_get_editlevel(acc) > 0) return;
    if (!priv->balance_dirty || priv->defer_bal_computation) return;
    if (qof_instance_get_destroying(acc)) return;
    if (qof_book_shutting_down(qof_instance_get_book(acc))) return;

    balance            = priv->starting_balance;
    noclosing_balance  = priv->starting_noclosing_balance;
    cleared_balance    = priv->starting_cleared_balance;
    reconciled_balance = priv->starting_reconciled_balance;

    PINFO ("acct=%s starting baln=%" G_GINT64_FORMAT "/%" G_GINT64_FORMAT,
           priv->accountName, balance.num, balance.denom);
    for (auto split : priv->splits)
    {
        gnc_numeric amt = xaccSplitGetAmount (split);

        balance = gnc_numeric_add_fixed(balance, amt);

        if (NREC != split->reconciled)
        {
            cleared_balance = gnc_numeric_add_fixed(cleared_balance, amt);
        }

        if (YREC == split->reconciled ||
                FREC == split->reconciled)
        {
            reconciled_balance =
                gnc_numeric_add_fixed(reconciled_balance, amt);
        }

        if (!(xaccTransGetIsClosingTxn (split->parent)))
            noclosing_balance = gnc_numeric_add_fixed(noclosing_balance, amt);

        split->balance = balance;
        split->noclosing_balance = noclosing_balance;
        split->cleared_balance = cleared_balance;
        split->reconciled_balance = reconciled_balance;

    }

    priv->balance = balance;
    priv->noclosing_balance = noclosing_balance;
    priv->cleared_balance = cleared_balance;
    priv->reconciled_balance = reconciled_balance;
    priv->balance_dirty = FALSE;
}

/********************************************************************\
\********************************************************************/

/* The sort order is used to implicitly define an
 * order for report generation */

static int typeorder[NUM_ACCOUNT_TYPES] =
{
    ACCT_TYPE_BANK, ACCT_TYPE_STOCK, ACCT_TYPE_MUTUAL, ACCT_TYPE_CURRENCY,
    ACCT_TYPE_CASH, ACCT_TYPE_ASSET, ACCT_TYPE_RECEIVABLE,
    ACCT_TYPE_CREDIT, ACCT_TYPE_LIABILITY, ACCT_TYPE_PAYABLE,
    ACCT_TYPE_INCOME, ACCT_TYPE_EXPENSE, ACCT_TYPE_EQUITY, ACCT_TYPE_TRADING
};

static int revorder[NUM_ACCOUNT_TYPES] =
{
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1
};


int
xaccAccountOrder (const Account *aa, const Account *ab)
{
    AccountPrivate *priv_aa, *priv_ab;
    const char *da, *db;
    int ta, tb, result;

    if ( aa && !ab ) return -1;
    if ( !aa && ab ) return +1;
    if ( !aa && !ab ) return 0;

    priv_aa = GET_PRIVATE(aa);
    priv_ab = GET_PRIVATE(ab);

    /* sort on accountCode strings */
    da = priv_aa->accountCode;
    db = priv_ab->accountCode;

    /* Otherwise do a string sort */
    result = g_strcmp0 (da, db);
    if (result)
        return result;

    /* if account-type-order array not initialized, initialize it */
    /* this will happen at most once during program invocation */
    if (-1 == revorder[0])
    {
        int i;
        for (i = 0; i < NUM_ACCOUNT_TYPES; i++)
        {
            revorder [typeorder[i]] = i;
        }
    }

    /* otherwise, sort on account type */
    ta = priv_aa->type;
    tb = priv_ab->type;
    ta = revorder[ta];
    tb = revorder[tb];
    if (ta < tb) return -1;
    if (ta > tb) return +1;

    /* otherwise, sort on accountName strings */
    da = priv_aa->accountName;
    db = priv_ab->accountName;
    result = safe_utf8_collate(da, db);
    if (result)
        return result;

    /* guarantee a stable sort */
    return qof_instance_guid_compare(aa, ab);
}

static int
qof_xaccAccountOrder (const Account **aa, const Account **ab)
{
    return xaccAccountOrder(*aa, *ab);
}

/********************************************************************\
\********************************************************************/

void
xaccAccountSetType (Account *acc, GNCAccountType tip)
{
    AccountPrivate *priv;

    /* errors */
    g_return_if_fail(GNC_IS_ACCOUNT(acc));
    g_return_if_fail(tip < NUM_ACCOUNT_TYPES);

    /* optimizations */
    priv = GET_PRIVATE(acc);
    if (priv->type == tip)
        return;

    xaccAccountBeginEdit(acc);
    priv->type = tip;
    priv->balance_dirty = TRUE; /* new type may affect balance computation */
    mark_account(acc);
    xaccAccountCommitEdit(acc);
}

void
xaccAccountSetName (Account *acc, const char *str)
{
    AccountPrivate *priv;

    /* errors */
    g_return_if_fail(GNC_IS_ACCOUNT(acc));
    g_return_if_fail(str);

    /* optimizations */
    priv = GET_PRIVATE(acc);
    if (g_strcmp0(str, priv->accountName) == 0)
        return;

    xaccAccountBeginEdit(acc);
    priv->accountName = qof_string_cache_replace(priv->accountName, str);
    mark_account (acc);
    xaccAccountCommitEdit(acc);
}

void
xaccAccountSetCode (Account *acc, const char *str)
{
    AccountPrivate *priv;

    /* errors */
    g_return_if_fail(GNC_IS_ACCOUNT(acc));

    /* optimizations */
    priv = GET_PRIVATE(acc);
    if (g_strcmp0(str, priv->accountCode) == 0)
        return;

    xaccAccountBeginEdit(acc);
    priv->accountCode = qof_string_cache_replace(priv->accountCode, str ? str : "");
    mark_account (acc);
    xaccAccountCommitEdit(acc);
}

void
xaccAccountSetDescription (Account *acc, const char *str)
{
    AccountPrivate *priv;

    /* errors */
    g_return_if_fail(GNC_IS_ACCOUNT(acc));

    /* optimizations */
    priv = GET_PRIVATE(acc);
    if (g_strcmp0(str, priv->description) == 0)
        return;

    xaccAccountBeginEdit(acc);
    priv->description = qof_string_cache_replace(priv->description, str ? str : "");
    mark_account (acc);
    xaccAccountCommitEdit(acc);
}

static void
set_kvp_gnc_numeric_path (Account *acc, const std::vector<std::string>& path,
                          std::optional<gnc_numeric> value)
{
    xaccAccountBeginEdit(acc);
    qof_instance_set_path_kvp<gnc_numeric> (QOF_INSTANCE(acc), value, path);
    xaccAccountCommitEdit(acc);
}

static std::optional<gnc_numeric>
get_kvp_gnc_numeric_path (const Account *acc, const Path& path)
{
    return qof_instance_get_path_kvp<gnc_numeric> (QOF_INSTANCE(acc), path);
}

static void
set_kvp_string_path (Account *acc, std::vector<std::string> const & path,
                     const char *value)
{
    std::optional<const char*> val;
    if (value && *value)
        val = g_strdup(value);

    xaccAccountBeginEdit(acc);
    qof_instance_set_path_kvp<const char*> (QOF_INSTANCE(acc), val, path);
    xaccAccountCommitEdit(acc);
}

static const char*
get_kvp_string_path (const Account *acc, const Path& path)
{
    auto rv{qof_instance_get_path_kvp<const char*> (QOF_INSTANCE(acc), path)};
    return rv ? *rv : nullptr;
}

static void
set_kvp_account_path (Account* acc, const Path& path, const Account* kvp_account)
{
    std::optional<GncGUID*> val;
    if (kvp_account)
        val = guid_copy(xaccAccountGetGUID (kvp_account));

    xaccAccountBeginEdit(acc);
    qof_instance_set_path_kvp<GncGUID*> (QOF_INSTANCE(acc), val, path);
    xaccAccountCommitEdit(acc);
}

static Account*
get_kvp_account_path (const Account *acc, const Path& path)
{
    auto val{qof_instance_get_path_kvp<GncGUID*> (QOF_INSTANCE(acc), path)};
    return val ? xaccAccountLookup (*val, gnc_account_get_book (acc)) : nullptr;
}

static void
set_kvp_boolean_path (Account *acc, const Path& path, gboolean option)
{
    set_kvp_string_path (acc, path, option ? "true" : nullptr);
}

static gboolean
get_kvp_boolean_path (const Account *acc, const Path& path)
{
    auto slot{QOF_INSTANCE(acc)->kvp_data->get_slot(path)};
    if (!slot) return false;
    switch (slot->get_type())
    {
    case KvpValueImpl::Type::INT64:
        return slot->get<int64_t>() != 0;
    case KvpValueImpl::Type::STRING:
        return g_strcmp0 (slot->get<const char*>(), "true") == 0;
    default:
        return false;
    }
}

static void
set_kvp_int64_path (Account *acc, const Path& path, std::optional<gint64> value)
{
    xaccAccountBeginEdit(acc);
    qof_instance_set_path_kvp<int64_t> (QOF_INSTANCE(acc), value, path);
    xaccAccountCommitEdit(acc);
}

static const std::optional<gint64>
get_kvp_int64_path (const Account *acc, const Path& path)
{
    return qof_instance_get_path_kvp<int64_t> (QOF_INSTANCE(acc), path);
}

void
xaccAccountSetColor (Account *acc, const char *str)
{
    set_kvp_string_path (acc, {"color"}, str);
}

void
xaccAccountSetFilter (Account *acc, const char *str)
{
    set_kvp_string_path (acc, {"filter"}, str);
}

void
xaccAccountSetSortOrder (Account *acc, const char *str)
{
    set_kvp_string_path (acc, {"sort-order"}, str);
}

void
xaccAccountSetSortReversed (Account *acc, gboolean sortreversed)
{
    set_kvp_boolean_path (acc, {"sort-reversed"}, sortreversed);
}

static void
qofAccountSetParent (Account *acc, QofInstance *parent)
{
    Account *parent_acc;

    g_return_if_fail(GNC_IS_ACCOUNT(acc));
    g_return_if_fail(GNC_IS_ACCOUNT(parent));

    parent_acc = GNC_ACCOUNT(parent);
    xaccAccountBeginEdit(acc);
    xaccAccountBeginEdit(parent_acc);
    gnc_account_append_child(parent_acc, acc);
    mark_account (parent_acc);
    mark_account (acc);
    xaccAccountCommitEdit(acc);
    xaccAccountCommitEdit(parent_acc);
}

void
xaccAccountSetNotes (Account *acc, const char *str)
{
    set_kvp_string_path (acc, {"notes"}, str);
}


void
xaccAccountSetAssociatedAccount (Account *acc, const char *tag, const Account* assoc_acct)
{
    g_return_if_fail (GNC_IS_ACCOUNT(acc));
    g_return_if_fail (tag && *tag);

    set_kvp_account_path (acc, {"associated-account", tag}, assoc_acct);
}

void
xaccAccountSetCommodity (Account * acc, gnc_commodity * com)
{
    AccountPrivate *priv;

    /* errors */
    g_return_if_fail(GNC_IS_ACCOUNT(acc));
    g_return_if_fail(GNC_IS_COMMODITY(com));

    /* optimizations */
    priv = GET_PRIVATE(acc);
    if (com == priv->commodity)
        return;

    xaccAccountBeginEdit(acc);
    gnc_commodity_decrement_usage_count(priv->commodity);
    priv->commodity = com;
    gnc_commodity_increment_usage_count(com);
    priv->commodity_scu = gnc_commodity_get_fraction(com);
    priv->non_standard_scu = FALSE;

    /* iterate over splits */
    for (auto s : priv->splits)
    {
        Transaction *trans = xaccSplitGetParent (s);

        xaccTransBeginEdit (trans);
        xaccSplitSetAmount (s, xaccSplitGetAmount(s));
        xaccTransCommitEdit (trans);
    }

    priv->sort_dirty = TRUE;  /* Not needed. */
    priv->balance_dirty = TRUE;
    mark_account (acc);

    xaccAccountCommitEdit(acc);
}

/*
 * Set the account scu and then check to see if it is the same as the
 * commodity scu.  This function is called when parsing the data file
 * and is designed to catch cases where the two were accidentally set
 * to mismatched values in the past.
 */
void
xaccAccountSetCommoditySCU (Account *acc, int scu)
{
    AccountPrivate *priv;

    g_return_if_fail(GNC_IS_ACCOUNT(acc));

    priv = GET_PRIVATE(acc);
    xaccAccountBeginEdit(acc);
    priv->commodity_scu = scu;
    if (scu != gnc_commodity_get_fraction(priv->commodity))
        priv->non_standard_scu = TRUE;
    mark_account(acc);
    xaccAccountCommitEdit(acc);
}

int
xaccAccountGetCommoditySCUi (const Account * acc)
{
    g_return_val_if_fail(GNC_IS_ACCOUNT(acc), 0);
    return GET_PRIVATE(acc)->commodity_scu;
}

int
xaccAccountGetCommoditySCU (const Account * acc)
{
    AccountPrivate *priv;

    g_return_val_if_fail(GNC_IS_ACCOUNT(acc), 0);

    priv = GET_PRIVATE(acc);
    if (priv->non_standard_scu || !priv->commodity)
        return priv->commodity_scu;
    return gnc_commodity_get_fraction(priv->commodity);
}

void
xaccAccountSetNonStdSCU (Account *acc, gboolean flag)
{
    AccountPrivate *priv;

    g_return_if_fail(GNC_IS_ACCOUNT(acc));

    priv = GET_PRIVATE(acc);
    if (priv->non_standard_scu == flag)
        return;
    xaccAccountBeginEdit(acc);
    priv->non_standard_scu = flag;
    mark_account (acc);
    xaccAccountCommitEdit(acc);
}

gboolean
xaccAccountGetNonStdSCU (const Account * acc)
{
    g_return_val_if_fail(GNC_IS_ACCOUNT(acc), 0);
    return GET_PRIVATE(acc)->non_standard_scu;
}

/********************************************************************\
\********************************************************************/
/* below follow the old, deprecated currency/security routines. */

void
DxaccAccountSetCurrency (Account * acc, gnc_commodity * currency)
{
    if ((!acc) || (!currency)) return;

    auto s = gnc_commodity_get_unique_name (currency);
    set_kvp_string_path (acc, {"old-currency"}, s);

    auto book = qof_instance_get_book(acc);
    auto table = gnc_commodity_table_get_table (book);
    auto commodity = gnc_commodity_table_lookup_unique (table, s);

    if (!commodity)
        gnc_commodity_table_insert (table, currency);
}

/********************************************************************\
\********************************************************************/

void
gnc_account_foreach_descendant (const Account *acc, std::function<void(Account*)> account_cb)
{
    g_return_if_fail (GNC_IS_ACCOUNT(acc));

    // children must be a vector copy instead of reference because
    // some callers e.g. xaccAccountTreeScrubLots will modify the
    // children
    auto children = GET_PRIVATE(acc)->children;
    for (auto child : children)
    {
        account_cb (child);
        gnc_account_foreach_descendant (child, account_cb);
    }
}

static void
account_foreach_descendant_sorted (const Account *acc, std::function<void(Account*)> account_cb)
{
    g_return_if_fail (GNC_IS_ACCOUNT(acc));

    auto children = GET_PRIVATE(acc)->children;
    std::sort (children.begin(), children.end(),
               [](auto a, auto b) { return xaccAccountOrder (a, b) < 0; });

    for (auto child : children)
    {
        account_cb (child);
        account_foreach_descendant_sorted (child, account_cb);
    }
}

void
gnc_account_append_child (Account *new_parent, Account *child)
{
    AccountPrivate *ppriv, *cpriv;
    Account *old_parent;
    QofCollection *col;

    /* errors */
    g_assert(GNC_IS_ACCOUNT(new_parent));
    g_assert(GNC_IS_ACCOUNT(child));

    /* optimizations */
    ppriv = GET_PRIVATE(new_parent);
    cpriv = GET_PRIVATE(child);
    old_parent = cpriv->parent;
    if (old_parent == new_parent)
        return;

    //  xaccAccountBeginEdit(new_parent);
    xaccAccountBeginEdit(child);
    if (old_parent)
    {
        gnc_account_remove_child(old_parent, child);

        if (!qof_instance_books_equal(old_parent, new_parent))
        {
            /* hack alert -- this implementation is not exactly correct.
             * If the entity tables are not identical, then the 'from' book
             * may have a different backend than the 'to' book.  This means
             * that we should get the 'from' backend to destroy this account,
             * and the 'to' backend to save it.  Right now, this is broken.
             *
             * A 'correct' implementation similar to this is in Period.c
             * except its for transactions ...
             *
             * Note also, we need to reparent the children to the new book as well.
             */
            PWARN ("reparenting accounts across books is not correctly supported\n");

            qof_event_gen (&child->inst, QOF_EVENT_DESTROY, nullptr);
            col = qof_book_get_collection (qof_instance_get_book(new_parent),
                                           GNC_ID_ACCOUNT);
            qof_collection_insert_entity (col, &child->inst);
            qof_event_gen (&child->inst, QOF_EVENT_CREATE, nullptr);
        }
    }
    cpriv->parent = new_parent;
    ppriv->children.push_back (child);
    qof_instance_set_dirty(&new_parent->inst);
    qof_instance_set_dirty(&child->inst);

    /* Send events data. Warning: The call to commit_edit is also going
     * to send a MODIFY event. If the gtktreemodelfilter code gets the
     * MODIFY before it gets the ADD, it gets very confused and thinks
     * that two nodes have been added. */
    qof_event_gen (&child->inst, QOF_EVENT_ADD, nullptr);
    // qof_event_gen (&new_parent->inst, QOF_EVENT_MODIFY, nullptr);

    xaccAccountCommitEdit (child);
    //  xaccAccountCommitEdit(new_parent);
}

void
gnc_account_remove_child (Account *parent, Account *child)
{
    AccountPrivate *ppriv, *cpriv;
    GncEventData ed;

    if (!child) return;

    /* Note this routine might be called on accounts which
     * are not yet parented. */
    if (!parent) return;

    ppriv = GET_PRIVATE(parent);
    cpriv = GET_PRIVATE(child);

    if (cpriv->parent != parent)
    {
        PERR ("account not a child of parent");
        return;
    }

    /* Gather event data */
    ed.node = parent;
    ed.idx = gnc_account_child_index (parent, child);

    ppriv->children.erase (std::remove (ppriv->children.begin(), ppriv->children.end(), child),
                           ppriv->children.end());

    /* Now send the event. */
    qof_event_gen(&child->inst, QOF_EVENT_REMOVE, &ed);

    /* clear the account's parent pointer after REMOVE event generation. */
    cpriv->parent = nullptr;

    qof_event_gen (&parent->inst, QOF_EVENT_MODIFY, nullptr);
}

Account *
gnc_account_get_parent (const Account *acc)
{
    g_return_val_if_fail(GNC_IS_ACCOUNT(acc), nullptr);
    return GET_PRIVATE(acc)->parent;
}

Account *
gnc_account_get_root (Account *acc)
{
    g_return_val_if_fail(GNC_IS_ACCOUNT(acc), nullptr);

    while (auto parent = gnc_account_get_parent (acc))
        acc = parent;

    return acc;
}

gboolean
gnc_account_is_root (const Account *account)
{
    g_return_val_if_fail(GNC_IS_ACCOUNT(account), FALSE);
    return (GET_PRIVATE(account)->parent == nullptr);
}

GList *
gnc_account_get_children (const Account *account)
{
    g_return_val_if_fail(GNC_IS_ACCOUNT(account), nullptr);
    auto& children = GET_PRIVATE(account)->children;
    return std::accumulate (children.rbegin(), children.rend(), static_cast<GList*>(nullptr),
                            g_list_prepend);
}

GList *
gnc_account_get_children_sorted (const Account *account)
{
    g_return_val_if_fail(GNC_IS_ACCOUNT(account), nullptr);
    return g_list_sort(gnc_account_get_children (account), (GCompareFunc)xaccAccountOrder);
}

gint
gnc_account_n_children (const Account *account)
{
    g_return_val_if_fail(GNC_IS_ACCOUNT(account), 0);
    return GET_PRIVATE(account)->children.size();
}

gint
gnc_account_child_index (const Account *parent, const Account *child)
{
    g_return_val_if_fail(GNC_IS_ACCOUNT(parent), -1);
    g_return_val_if_fail(GNC_IS_ACCOUNT(child), -1);
    auto& children = GET_PRIVATE(parent)->children;
    return std::distance (children.begin(), std::find (children.begin(), children.end(), child));
}

Account *
gnc_account_nth_child (const Account *parent, gint num)
{
    g_return_val_if_fail(GNC_IS_ACCOUNT(parent), nullptr);
    if ((size_t)num >= GET_PRIVATE(parent)->children.size())
        return nullptr;
    return static_cast<Account*>(GET_PRIVATE(parent)->children.at (num));
}

gint
gnc_account_n_descendants (const Account *account)
{
    int count {0};
    gnc_account_foreach_descendant (account, [&count](auto acc){ count++; });
    return count;
}

gint
gnc_account_get_current_depth (const Account *account)
{
    AccountPrivate *priv;
    int depth = 0;

    g_return_val_if_fail(GNC_IS_ACCOUNT(account), 0);

    priv = GET_PRIVATE(account);
    while (priv->parent && (priv->type != ACCT_TYPE_ROOT))
    {
        account = priv->parent;
        priv = GET_PRIVATE(account);
        depth++;
    }

    return depth;
}

gint
gnc_account_get_tree_depth (const Account *account)
{
    AccountPrivate *priv;
    g_return_val_if_fail(GNC_IS_ACCOUNT(account), 0);

    priv = GET_PRIVATE(account);
    if (!priv->children.size())
        return 1;

    return 1 + std::accumulate (priv->children.begin(), priv->children.end(),
                                0, [](auto a, auto b)
                                { return std::max (a, gnc_account_get_tree_depth (b)); });
}

GList *
gnc_account_get_descendants (const Account *account)
{
    GList* list = nullptr;
    gnc_account_foreach_descendant (account, [&list](auto a){ list = g_list_prepend (list, a); });
    return g_list_reverse (list);
}

GList *
gnc_account_get_descendants_sorted (const Account *account)
{
    GList* list = nullptr;
    account_foreach_descendant_sorted (account, [&list](auto a){ list = g_list_prepend (list, a); });
    return g_list_reverse (list);
}

// because gnc_account_lookup_by_name and gnc_account_lookup_by_code
// are described in Account.h searching breadth-first until 4.6, and
// accidentally modified to search depth-first from 4.7
// onwards. Restore breath-first searching in 4.11 onwards to match
// previous behaviour and function description in Account.h
static gpointer
account_foreach_descendant_breadthfirst_until (const Account *acc,
                                               AccountCb2 thunk,
                                               gpointer user_data)
{
    g_return_val_if_fail (GNC_IS_ACCOUNT(acc), nullptr);
    g_return_val_if_fail (thunk, nullptr);

    auto& children{GET_PRIVATE(acc)->children};

    for (auto acc : children)
        if (auto result = thunk (acc, user_data))
            return result;

    for (auto acc: children)
        if (auto result = account_foreach_descendant_breadthfirst_until (acc, thunk, user_data))
            return result;

    return nullptr;
}

static gpointer
is_acct_name (Account *account, gpointer user_data)
{
    auto name {static_cast<gchar*>(user_data)};
    return (g_strcmp0 (name, xaccAccountGetName (account)) ? nullptr : account);
}

Account *
gnc_account_lookup_by_name (const Account *parent, const char * name)
{
    return (Account*)account_foreach_descendant_breadthfirst_until (parent, is_acct_name, (char*)name);
}

static gpointer
is_acct_code (Account *account, gpointer user_data)
{
    auto name {static_cast<gchar*>(user_data)};
    return (g_strcmp0 (name, xaccAccountGetCode (account)) ? nullptr : account);
}

Account *
gnc_account_lookup_by_code (const Account *parent, const char * code)
{
    return (Account*)account_foreach_descendant_breadthfirst_until (parent, is_acct_code, (char*)code);
}

static gpointer
is_opening_balance_account (Account* account, gpointer data)
{
    gnc_commodity* commodity = GNC_COMMODITY(data);
    if (xaccAccountGetIsOpeningBalance(account) && gnc_commodity_equiv(commodity, xaccAccountGetCommodity(account)))
        return account;
    return nullptr;
}

Account*
gnc_account_lookup_by_opening_balance (Account* account, gnc_commodity* commodity)
{
    return (Account *)gnc_account_foreach_descendant_until (account, is_opening_balance_account, commodity);
}

/********************************************************************\
 * Fetch an account, given its full name                            *
\********************************************************************/

static Account *
gnc_account_lookup_by_full_name_helper (const Account *parent,
                                        gchar **names)
{
    g_return_val_if_fail(GNC_IS_ACCOUNT(parent), nullptr);
    g_return_val_if_fail(names, nullptr);

    /* Look for the first name in the children. */
    for (auto account : GET_PRIVATE(parent)->children)
    {
        auto priv = GET_PRIVATE(account);
        if (g_strcmp0(priv->accountName, names[0]) == 0)
        {
            /* We found an account.  If the next entry is nullptr, there is
             * nothing left in the name, so just return the account. */
            if (names[1] == nullptr)
                return account;

            /* No children?  We're done. */
            if (priv->children.empty())
                return nullptr;

            /* There's stuff left to search for.  Search recursively. */
            if (auto found = gnc_account_lookup_by_full_name_helper(account, &names[1]))
                return found;
        }
    }

    return nullptr;
}


Account *
gnc_account_lookup_by_full_name (const Account *any_acc,
                                 const gchar *name)
{
    const AccountPrivate *rpriv;
    const Account *root;
    Account *found;
    gchar **names;

    g_return_val_if_fail(GNC_IS_ACCOUNT(any_acc), nullptr);
    g_return_val_if_fail(name, nullptr);

    root = any_acc;
    rpriv = GET_PRIVATE(root);
    while (rpriv->parent)
    {
        root = rpriv->parent;
        rpriv = GET_PRIVATE(root);
    }
    names = g_strsplit(name, gnc_get_account_separator_string(), -1);
    found = gnc_account_lookup_by_full_name_helper(root, names);
    g_strfreev(names);
    return found;
}

GList*
gnc_account_lookup_by_type_and_commodity (Account* root,
                                          const char* name,
                                          GNCAccountType acctype,
                                          gnc_commodity* commodity)
{
    GList *retval{};
    auto rpriv{GET_PRIVATE(root)};
    for (auto account : rpriv->children)
    {
        if (xaccAccountGetType (account) == acctype)
        {
            if (commodity &&
                !gnc_commodity_equiv(xaccAccountGetCommodity (account),
                                     commodity))
                continue;

            if (name && strcmp(name, xaccAccountGetName(account)))
                continue;

            retval = g_list_prepend(retval, account);
        }
    }

    if (!retval) // Recurse through the children
        for (auto account : rpriv->children)
        {
            auto result = gnc_account_lookup_by_type_and_commodity(account,
                                                                   name,
                                                                   acctype,
                                                                   commodity);
            if (result)
                retval = g_list_concat(result, retval);
        }
    return retval;
}

void
gnc_account_foreach_child (const Account *acc,
                           AccountCb thunk,
                           gpointer user_data)
{
    g_return_if_fail(GNC_IS_ACCOUNT(acc));
    g_return_if_fail(thunk);
    std::for_each (GET_PRIVATE(acc)->children.begin(), GET_PRIVATE(acc)->children.end(),
                   [user_data, thunk](auto a){ thunk (a, user_data); });
}

void
gnc_account_foreach_descendant (const Account *acc,
                                AccountCb thunk,
                                gpointer user_data)
{
    gnc_account_foreach_descendant (acc, [&](auto acc){ thunk (acc, user_data); });
}

gpointer
gnc_account_foreach_descendant_until (const Account *acc,
                                      AccountCb2 thunk,
                                      gpointer user_data)
{
    gpointer result {nullptr};

    g_return_val_if_fail (GNC_IS_ACCOUNT(acc), nullptr);
    g_return_val_if_fail (thunk, nullptr);

    for (auto child : GET_PRIVATE(acc)->children)
    {
        result = thunk (child, user_data);
        if (result) break;

        result = gnc_account_foreach_descendant_until (child, thunk, user_data);
        if (result) break;
    }

    return result;
}


GNCAccountType
xaccAccountGetType (const Account *acc)
{
    g_return_val_if_fail(GNC_IS_ACCOUNT(acc), ACCT_TYPE_NONE);
    return GET_PRIVATE(acc)->type;
}

static const char*
qofAccountGetTypeString (const Account *acc)
{
    g_return_val_if_fail(GNC_IS_ACCOUNT(acc), nullptr);
    return xaccAccountTypeEnumAsString(GET_PRIVATE(acc)->type);
}

static void
qofAccountSetType (Account *acc, const char *type_string)
{
    g_return_if_fail(GNC_IS_ACCOUNT(acc));
    g_return_if_fail(type_string);
    xaccAccountSetType(acc, xaccAccountStringToEnum(type_string));
}

const char *
xaccAccountGetName (const Account *acc)
{
    g_return_val_if_fail(GNC_IS_ACCOUNT(acc), nullptr);
    return GET_PRIVATE(acc)->accountName;
}

gchar *
gnc_account_get_full_name(const Account *account)
{
    AccountPrivate *priv;
    const Account *a;
    char *fullname;
    const gchar **names;
    int level;

    /* So much for hardening the API. Too many callers to this function don't
     * bother to check if they have a non-nullptr pointer before calling. */
    if (nullptr == account)
        return g_strdup("");

    /* errors */
    g_return_val_if_fail(GNC_IS_ACCOUNT(account), g_strdup(""));

    /* optimizations */
    priv = GET_PRIVATE(account);
    if (!priv->parent)
        return g_strdup("");

    /* Figure out how much space is needed by counting the nodes up to
     * the root. */
    level = 0;
    for (a = account; a; a = priv->parent)
    {
        priv = GET_PRIVATE(a);
        level++;
    }

    /* Get all the pointers in the right order. The root node "entry"
     * becomes the terminating nullptr pointer for the array of strings. */
    names = (const gchar **)g_malloc(level * sizeof(gchar *));
    names[--level] = nullptr;
    for (a = account; level > 0; a = priv->parent)
    {
        priv = GET_PRIVATE(a);
        names[--level] = priv->accountName;
    }

    /* Build the full name */
    fullname = g_strjoinv(account_separator, (gchar **)names);
    g_free(names);

    return fullname;
}

const char *
xaccAccountGetCode (const Account *acc)
{
    g_return_val_if_fail(GNC_IS_ACCOUNT(acc), nullptr);
    return GET_PRIVATE(acc)->accountCode;
}

const char *
xaccAccountGetDescription (const Account *acc)
{
    g_return_val_if_fail(GNC_IS_ACCOUNT(acc), nullptr);
    return GET_PRIVATE(acc)->description;
}

const char *
xaccAccountGetColor (const Account *acc)
{
    return get_kvp_string_path (acc, {"color"});
}

const char *
xaccAccountGetFilter (const Account *acc)
{
    return get_kvp_string_path (acc, {"filter"});
}

const char *
xaccAccountGetSortOrder (const Account *acc)
{
    return get_kvp_string_path (acc, {"sort-order"});
}

gboolean
xaccAccountGetSortReversed (const Account *acc)
{
    return get_kvp_boolean_path (acc, {"sort-reversed"});
}

const char *
xaccAccountGetNotes (const Account *acc)
{
    return get_kvp_string_path (acc, {"notes"});
}

Account*
xaccAccountGetAssociatedAccount (const Account *acc, const char *tag)
{
    g_return_val_if_fail (tag && *tag, nullptr);

    return get_kvp_account_path (acc, {"associated-account", tag});
}


gnc_commodity *
DxaccAccountGetCurrency (const Account *acc)
{
    if (auto s = get_kvp_string_path (acc, {"old-currency"}))
    {
        auto table = gnc_commodity_table_get_table (qof_instance_get_book(acc));
        return gnc_commodity_table_lookup_unique (table, s);
    }

    return nullptr;
}

gnc_commodity *
xaccAccountGetCommodity (const Account *acc)
{
    if (!GNC_IS_ACCOUNT(acc))
        return nullptr;
    return GET_PRIVATE(acc)->commodity;
}

gnc_commodity * gnc_account_get_currency_or_parent(const Account* account)
{
    g_return_val_if_fail (GNC_IS_ACCOUNT (account), nullptr);

    for (auto acc = account; acc; acc = gnc_account_get_parent (acc))
        if (auto comm = xaccAccountGetCommodity (acc); gnc_commodity_is_currency (comm))
            return comm;

    return nullptr; // no suitable commodity found.
}

/********************************************************************\
\********************************************************************/
void
gnc_account_set_start_balance (Account *acc, const gnc_numeric start_baln)
{
    AccountPrivate *priv;

    g_return_if_fail(GNC_IS_ACCOUNT(acc));

    priv = GET_PRIVATE(acc);
    priv->starting_balance = start_baln;
    priv->balance_dirty = TRUE;
}

void
gnc_account_set_start_cleared_balance (Account *acc,
                                       const gnc_numeric start_baln)
{
    AccountPrivate *priv;

    g_return_if_fail(GNC_IS_ACCOUNT(acc));

    priv = GET_PRIVATE(acc);
    priv->starting_cleared_balance = start_baln;
    priv->balance_dirty = TRUE;
}

void
gnc_account_set_start_reconciled_balance (Account *acc,
        const gnc_numeric start_baln)
{
    AccountPrivate *priv;

    g_return_if_fail(GNC_IS_ACCOUNT(acc));

    priv = GET_PRIVATE(acc);
    priv->starting_reconciled_balance = start_baln;
    priv->balance_dirty = TRUE;
}

gnc_numeric
xaccAccountGetBalance (const Account *acc)
{
    g_return_val_if_fail(GNC_IS_ACCOUNT(acc), gnc_numeric_zero());
    return GET_PRIVATE(acc)->balance;
}

gnc_numeric
xaccAccountGetClearedBalance (const Account *acc)
{
    g_return_val_if_fail(GNC_IS_ACCOUNT(acc), gnc_numeric_zero());
    return GET_PRIVATE(acc)->cleared_balance;
}

gnc_numeric
xaccAccountGetReconciledBalance (const Account *acc)
{
    g_return_val_if_fail(GNC_IS_ACCOUNT(acc), gnc_numeric_zero());
    return GET_PRIVATE(acc)->reconciled_balance;
}

gnc_numeric
xaccAccountGetProjectedMinimumBalance (const Account *acc)
{
    auto today{gnc_time64_get_today_end()};
    std::optional<gnc_numeric> minimum;

    auto before_today_end = [&minimum, today](const Split *s) -> bool
    {
        auto bal{xaccSplitGetBalance(s)};
        if (!minimum || gnc_numeric_compare (bal, *minimum) < 0)
            minimum = bal;
        return (xaccTransGetDate(xaccSplitGetParent(s)) < today);
    };
    // scan to find today's split, but we're really interested in the
    // minimum balance
    [[maybe_unused]] auto todays_split = gnc_account_find_split (acc, before_today_end, true);
    return minimum ? *minimum : gnc_numeric_zero();
}


/********************************************************************\
\********************************************************************/

static gnc_numeric
GetBalanceAsOfDate (Account *acc, time64 date, std::function<gnc_numeric(Split*)> split_to_numeric)
{
    g_return_val_if_fail(GNC_IS_ACCOUNT(acc), gnc_numeric_zero());

    xaccAccountSortSplits (acc, TRUE); /* just in case, normally a noop */
    xaccAccountRecomputeBalance (acc); /* just in case, normally a noop */

    auto is_before_date = [date](auto s) -> bool
    { return xaccTransGetDate(xaccSplitGetParent(s)) < date; };

    auto latest_split{gnc_account_find_split (acc, is_before_date, true)};
    return latest_split ? split_to_numeric (latest_split) : gnc_numeric_zero();
}

gnc_numeric
xaccAccountGetBalanceAsOfDate (Account *acc, time64 date)
{
    return GetBalanceAsOfDate (acc, date, xaccSplitGetBalance);
}

static gnc_numeric
xaccAccountGetNoclosingBalanceAsOfDate (Account *acc, time64 date)
{
    return GetBalanceAsOfDate (acc, date, xaccSplitGetNoclosingBalance);
}

gnc_numeric
xaccAccountGetReconciledBalanceAsOfDate (Account *acc, time64 date)
{
    return GetBalanceAsOfDate (acc, date, xaccSplitGetReconciledBalance);
}

/*
 * Originally gsr_account_present_balance in gnc-split-reg.c
 */
gnc_numeric
xaccAccountGetPresentBalance (const Account *acc)
{
    g_return_val_if_fail(GNC_IS_ACCOUNT(acc), gnc_numeric_zero());

    return xaccAccountGetBalanceAsOfDate (GNC_ACCOUNT (acc),
                                          gnc_time64_get_today_end ());
}


/********************************************************************\
\********************************************************************/
/* XXX TODO: These 'GetBal' routines should be moved to some
 * utility area outside of the core account engine area.
 */

/*
 * Convert a balance from one currency to another.
 */
gnc_numeric
xaccAccountConvertBalanceToCurrency(const Account *acc, /* for book */
                                    gnc_numeric balance,
                                    const gnc_commodity *balance_currency,
                                    const gnc_commodity *new_currency)
{
    QofBook *book;
    GNCPriceDB *pdb;

    if (gnc_numeric_zero_p (balance) ||
            gnc_commodity_equiv (balance_currency, new_currency))
        return balance;

    book = gnc_account_get_book (acc);
    pdb = gnc_pricedb_get_db (book);

    balance = gnc_pricedb_convert_balance_latest_price(
                  pdb, balance, balance_currency, new_currency);

    return balance;
}

/*
 * Convert a balance from one currency to another with price of
 * a given date.
 */
gnc_numeric
xaccAccountConvertBalanceToCurrencyAsOfDate(const Account *acc, /* for book */
        gnc_numeric balance,
        const gnc_commodity *balance_currency,
        const gnc_commodity *new_currency,
        time64 date)
{
    QofBook *book;
    GNCPriceDB *pdb;

    if (gnc_numeric_zero_p (balance) ||
            gnc_commodity_equiv (balance_currency, new_currency))
        return balance;

    book = gnc_account_get_book (acc);
    pdb = gnc_pricedb_get_db (book);

    balance = gnc_pricedb_convert_balance_nearest_before_price_t64 (
                  pdb, balance, balance_currency, new_currency, date);

    return balance;
}

/*
 * Given an account and a GetBalanceFn pointer, extract the requested
 * balance from the account and then convert it to the desired
 * currency.
 */
static gnc_numeric
xaccAccountGetXxxBalanceInCurrency (const Account *acc,
                                    xaccGetBalanceFn fn,
                                    const gnc_commodity *report_currency)
{
    AccountPrivate *priv;
    gnc_numeric balance;

    g_return_val_if_fail(GNC_IS_ACCOUNT(acc), gnc_numeric_zero());
    g_return_val_if_fail(fn, gnc_numeric_zero());
    g_return_val_if_fail(GNC_IS_COMMODITY(report_currency), gnc_numeric_zero());

    priv = GET_PRIVATE(acc);
    balance = fn(acc);
    balance = xaccAccountConvertBalanceToCurrency(acc, balance,
              priv->commodity,
              report_currency);
    return balance;
}

static gnc_numeric
xaccAccountGetXxxBalanceAsOfDateInCurrency(Account *acc, time64 date,
        xaccGetBalanceAsOfDateFn fn,
        const gnc_commodity *report_commodity)
{
    AccountPrivate *priv;

    g_return_val_if_fail(GNC_IS_ACCOUNT(acc), gnc_numeric_zero());
    g_return_val_if_fail(fn, gnc_numeric_zero());
    g_return_val_if_fail(GNC_IS_COMMODITY(report_commodity), gnc_numeric_zero());

    priv = GET_PRIVATE(acc);
    return xaccAccountConvertBalanceToCurrencyAsOfDate(
               acc, fn(acc, date), priv->commodity, report_commodity, date);
}

/*
 * Data structure used to pass various arguments into the following fn.
 */
typedef struct
{
    const gnc_commodity *currency;
    gnc_numeric balance;
    xaccGetBalanceFn fn;
    xaccGetBalanceAsOfDateFn asOfDateFn;
    time64 date;
} CurrencyBalance;


/*
 * A helper function for iterating over all the accounts in a list or
 * tree.  This function is called once per account, and sums up the
 * values of all these accounts.
 */
static void
xaccAccountBalanceHelper (Account *acc, gpointer data)
{
    CurrencyBalance *cb = static_cast<CurrencyBalance*>(data);
    gnc_numeric balance;

    if (!cb->fn || !cb->currency)
        return;
    balance = xaccAccountGetXxxBalanceInCurrency (acc, cb->fn, cb->currency);
    cb->balance = gnc_numeric_add (cb->balance, balance,
                                   gnc_commodity_get_fraction (cb->currency),
                                   GNC_HOW_RND_ROUND_HALF_UP);
}

static void
xaccAccountBalanceAsOfDateHelper (Account *acc, gpointer data)
{
    CurrencyBalance *cb = static_cast<CurrencyBalance*>(data);
    gnc_numeric balance;

    g_return_if_fail (cb->asOfDateFn && cb->currency);

    balance = xaccAccountGetXxxBalanceAsOfDateInCurrency (
                  acc, cb->date, cb->asOfDateFn, cb->currency);
    cb->balance = gnc_numeric_add (cb->balance, balance,
                                   gnc_commodity_get_fraction (cb->currency),
                                   GNC_HOW_RND_ROUND_HALF_UP);
}



/*
 * Common function that iterates recursively over all accounts below
 * the specified account.  It uses xaccAccountBalanceHelper to sum up
 * the balances of all its children, and uses the specified function
 * 'fn' for extracting the balance.  This function may extract the
 * current value, the reconciled value, etc.
 *
 * If 'report_commodity' is nullptr, just use the account's commodity.
 * If 'include_children' is FALSE, this function doesn't recurse at all.
 */
static gnc_numeric
xaccAccountGetXxxBalanceInCurrencyRecursive (const Account *acc,
        xaccGetBalanceFn fn,
        const gnc_commodity *report_commodity,
        gboolean include_children)
{
    gnc_numeric balance;

    if (!acc) return gnc_numeric_zero ();
    if (!report_commodity)
        report_commodity = xaccAccountGetCommodity (acc);
    if (!report_commodity)
        return gnc_numeric_zero();

    balance = xaccAccountGetXxxBalanceInCurrency (acc, fn, report_commodity);

    /* If needed, sum up the children converting to the *requested*
       commodity. */
    if (include_children)
    {
#ifdef _MSC_VER
        /* MSVC compiler: Somehow, the struct initialization containing a
           gnc_numeric doesn't work. As an exception, we hand-initialize
           that member afterwards. */
        CurrencyBalance cb = { report_commodity, { 0 }, fn, nullptr, 0 };
        cb.balance = balance;
#else
        CurrencyBalance cb = { report_commodity, balance, fn, nullptr, 0 };
#endif

        gnc_account_foreach_descendant (acc, xaccAccountBalanceHelper, &cb);
        balance = cb.balance;
    }

    return balance;
}

static gnc_numeric
xaccAccountGetXxxBalanceAsOfDateInCurrencyRecursive (
    Account *acc, time64 date, xaccGetBalanceAsOfDateFn fn,
    const gnc_commodity *report_commodity, gboolean include_children)
{
    gnc_numeric balance;

    g_return_val_if_fail(acc, gnc_numeric_zero());
    if (!report_commodity)
        report_commodity = xaccAccountGetCommodity (acc);
    if (!report_commodity)
        return gnc_numeric_zero();

    balance = xaccAccountGetXxxBalanceAsOfDateInCurrency(
                  acc, date, fn, report_commodity);

    /* If needed, sum up the children converting to the *requested*
       commodity. */
    if (include_children)
    {
#ifdef _MSC_VER
        /* MSVC compiler: Somehow, the struct initialization containing a
           gnc_numeric doesn't work. As an exception, we hand-initialize
           that member afterwards. */
        CurrencyBalance cb = { report_commodity, 0, nullptr, fn, date };
        cb.balance = balance;
#else
        CurrencyBalance cb = { report_commodity, balance, nullptr, fn, date };
#endif

        gnc_account_foreach_descendant (acc, xaccAccountBalanceAsOfDateHelper, &cb);
        balance = cb.balance;
    }

    return balance;
}

gnc_numeric
xaccAccountGetBalanceInCurrency (const Account *acc,
                                 const gnc_commodity *report_commodity,
                                 gboolean include_children)
{
    gnc_numeric rc;
    rc = xaccAccountGetXxxBalanceInCurrencyRecursive (
             acc, xaccAccountGetBalance, report_commodity, include_children);
    PINFO(" baln=%" G_GINT64_FORMAT "/%" G_GINT64_FORMAT, rc.num, rc.denom);
    return rc;
}


gnc_numeric
xaccAccountGetClearedBalanceInCurrency (const Account *acc,
                                        const gnc_commodity *report_commodity,
                                        gboolean include_children)
{
    return xaccAccountGetXxxBalanceInCurrencyRecursive (
               acc, xaccAccountGetClearedBalance, report_commodity,
               include_children);
}

gnc_numeric
xaccAccountGetReconciledBalanceInCurrency (const Account *acc,
        const gnc_commodity *report_commodity,
        gboolean include_children)
{
    return xaccAccountGetXxxBalanceInCurrencyRecursive (
               acc, xaccAccountGetReconciledBalance, report_commodity,
               include_children);
}

gnc_numeric
xaccAccountGetPresentBalanceInCurrency (const Account *acc,
                                        const gnc_commodity *report_commodity,
                                        gboolean include_children)
{
    return xaccAccountGetXxxBalanceAsOfDateInCurrencyRecursive (
               (Account*)acc, gnc_time64_get_today_end (), xaccAccountGetBalanceAsOfDate,
               report_commodity,
               include_children);
}

gnc_numeric
xaccAccountGetProjectedMinimumBalanceInCurrency (
    const Account *acc,
    const gnc_commodity *report_commodity,
    gboolean include_children)
{
    return xaccAccountGetXxxBalanceInCurrencyRecursive (
               acc, xaccAccountGetProjectedMinimumBalance, report_commodity,
               include_children);
}

gnc_numeric
xaccAccountGetBalanceAsOfDateInCurrency(
    Account *acc, time64 date, gnc_commodity *report_commodity,
    gboolean include_children)
{
    return xaccAccountGetXxxBalanceAsOfDateInCurrencyRecursive (
               acc, date, xaccAccountGetBalanceAsOfDate, report_commodity,
               include_children);
}

gnc_numeric
xaccAccountGetNoclosingBalanceAsOfDateInCurrency(
    Account *acc, time64 date, gnc_commodity *report_commodity,
    gboolean include_children)
{
    return xaccAccountGetXxxBalanceAsOfDateInCurrencyRecursive
      (acc, date, xaccAccountGetNoclosingBalanceAsOfDate,
       report_commodity, include_children);
}

gnc_numeric
xaccAccountGetBalanceChangeForPeriod (Account *acc, time64 t1, time64 t2,
                                      gboolean recurse)
{
    gnc_numeric b1, b2;

    b1 = xaccAccountGetBalanceAsOfDateInCurrency(acc, t1, nullptr, recurse);
    b2 = xaccAccountGetBalanceAsOfDateInCurrency(acc, t2, nullptr, recurse);
    return gnc_numeric_sub(b2, b1, GNC_DENOM_AUTO, GNC_HOW_DENOM_FIXED);
}

gnc_numeric
xaccAccountGetNoclosingBalanceChangeForPeriod (Account *acc, time64 t1,
                                               time64 t2, gboolean recurse)
{
    gnc_numeric b1, b2;

    b1 = xaccAccountGetNoclosingBalanceAsOfDateInCurrency(acc, t1, nullptr, recurse);
    b2 = xaccAccountGetNoclosingBalanceAsOfDateInCurrency(acc, t2, nullptr, recurse);
    return gnc_numeric_sub(b2, b1, GNC_DENOM_AUTO, GNC_HOW_DENOM_FIXED);
}

typedef struct
{
    const gnc_commodity *currency;
    gnc_numeric balanceChange;
    time64 t1;
    time64 t2;
} CurrencyBalanceChange;

static void
xaccAccountBalanceChangeHelper (Account *acc, gpointer data)
{
    CurrencyBalanceChange *cbdiff = static_cast<CurrencyBalanceChange*>(data);

    gnc_numeric b1, b2;
    b1 = GetBalanceAsOfDate(acc, cbdiff->t1, xaccSplitGetNoclosingBalance);
    b2 = GetBalanceAsOfDate(acc, cbdiff->t2, xaccSplitGetNoclosingBalance);
    gnc_numeric balanceChange = gnc_numeric_sub(b2, b1, GNC_DENOM_AUTO, GNC_HOW_DENOM_FIXED);
    gnc_numeric balanceChange_conv = xaccAccountConvertBalanceToCurrencyAsOfDate(acc, balanceChange, xaccAccountGetCommodity(acc), cbdiff->currency, cbdiff->t2);
    cbdiff->balanceChange = gnc_numeric_add (cbdiff->balanceChange, balanceChange_conv,
                                gnc_commodity_get_fraction (cbdiff->currency),
                                GNC_HOW_RND_ROUND_HALF_UP);
}

gnc_numeric
xaccAccountGetNoclosingBalanceChangeInCurrencyForPeriod (Account *acc, time64 t1,
                                               time64 t2, gboolean recurse)
{
    

    gnc_numeric b1, b2;
    b1 = GetBalanceAsOfDate(acc, t1, xaccSplitGetNoclosingBalance);
    b2 = GetBalanceAsOfDate(acc, t2, xaccSplitGetNoclosingBalance);
    gnc_numeric balanceChange = gnc_numeric_sub(b2, b1, GNC_DENOM_AUTO, GNC_HOW_DENOM_FIXED);

    gnc_commodity *report_commodity = xaccAccountGetCommodity(acc);
    CurrencyBalanceChange cbdiff = { report_commodity, balanceChange, t1, t2 };

    if(recurse)
    {
        gnc_account_foreach_descendant (acc, xaccAccountBalanceChangeHelper, &cbdiff);
        balanceChange = cbdiff.balanceChange;
    }
    return balanceChange;
}

/********************************************************************\
\********************************************************************/

const SplitsVec&
xaccAccountGetSplits (const Account *account)
{
    static const SplitsVec empty;
    g_return_val_if_fail (GNC_IS_ACCOUNT(account), empty);
    return GET_PRIVATE(account)->splits;
}

SplitList *
xaccAccountGetSplitList (const Account *acc)
{
    g_return_val_if_fail(GNC_IS_ACCOUNT(acc), nullptr);
    auto priv{GET_PRIVATE(acc)};
    return std::accumulate (priv->splits.rbegin(), priv->splits.rend(),
                            static_cast<GList*>(nullptr), g_list_prepend);
}

size_t
xaccAccountGetSplitsSize (const Account *account)
{
    g_return_val_if_fail (GNC_IS_ACCOUNT(account), 0);
    return GNC_IS_ACCOUNT(account) ? GET_PRIVATE(account)->splits.size() : 0;
}

gboolean gnc_account_and_descendants_empty (Account *acc)
{
    g_return_val_if_fail (GNC_IS_ACCOUNT (acc), FALSE);
    auto priv = GET_PRIVATE (acc);
    if (!priv->splits.empty()) return FALSE;
    return std::all_of (priv->children.begin(), priv->children.end(),
                        gnc_account_and_descendants_empty);
}

LotList *
xaccAccountGetLotList (const Account *acc)
{
    g_return_val_if_fail(GNC_IS_ACCOUNT(acc), nullptr);
    return g_list_copy(GET_PRIVATE(acc)->lots);
}

LotList *
xaccAccountFindOpenLots (const Account *acc,
                         gboolean (*match_func)(GNCLot *lot,
                                 gpointer user_data),
                         gpointer user_data, GCompareFunc sort_func)
{
    AccountPrivate *priv;
    GList *lot_list;
    GList *retval = nullptr;

    g_return_val_if_fail(GNC_IS_ACCOUNT(acc), nullptr);

    priv = GET_PRIVATE(acc);
    for (lot_list = priv->lots; lot_list; lot_list = lot_list->next)
    {
        GNCLot *lot = static_cast<GNCLot*>(lot_list->data);

        /* If this lot is closed, then ignore it */
        if (gnc_lot_is_closed (lot))
            continue;

        if (match_func && !(match_func)(lot, user_data))
            continue;

        /* Ok, this is a valid lot.  Add it to our list of lots */
        retval = g_list_prepend (retval, lot);
    }

    if (sort_func)
        retval = g_list_sort (retval, sort_func);

    return retval;
}

gpointer
xaccAccountForEachLot(const Account *acc,
                      gpointer (*proc)(GNCLot *lot, void *data), void *data)
{
    g_return_val_if_fail(GNC_IS_ACCOUNT(acc), nullptr);
    g_return_val_if_fail(proc, nullptr);

    for (auto node = GET_PRIVATE(acc)->lots; node; node = node->next)
        if (auto result = proc(GNC_LOT(node->data), data))
            return result;

    return nullptr;
}


/********************************************************************\
\********************************************************************/

/* These functions use interchange gint64 and gboolean.  Is that right? */
gboolean
xaccAccountGetTaxRelated (const Account *acc)
{
    return get_kvp_boolean_path(acc, {"tax-related"});
}

void
xaccAccountSetTaxRelated (Account *acc, gboolean tax_related)
{
    set_kvp_boolean_path(acc, {"tax-related"}, tax_related);
}

const char *
xaccAccountGetTaxUSCode (const Account *acc)
{
    return get_kvp_string_path (acc, {"tax-US", "code"});
}

void
xaccAccountSetTaxUSCode (Account *acc, const char *code)
{
    set_kvp_string_path (acc, {"tax-US", "code"}, code);
}

const char *
xaccAccountGetTaxUSPayerNameSource (const Account *acc)
{
    return get_kvp_string_path (acc, {"tax-US", "payer-name-source"});
}

void
xaccAccountSetTaxUSPayerNameSource (Account *acc, const char *source)
{
    set_kvp_string_path (acc, {"tax-US", "payer-name-source"}, source);
}

gint64
xaccAccountGetTaxUSCopyNumber (const Account *acc)
{
    auto copy_number = get_kvp_int64_path (acc, {"tax-US", "copy-number"});
    return copy_number ? *copy_number : 1;
}

void
xaccAccountSetTaxUSCopyNumber (Account *acc, gint64 copy_number)
{
    set_kvp_int64_path (acc, {"tax-US", "copy-number"}, copy_number);
}

/*********************************************************************\
\ ********************************************************************/


const char *gnc_account_get_debit_string (GNCAccountType acct_type)
{
    if (gnc_prefs_get_bool(GNC_PREFS_GROUP_GENERAL, GNC_PREF_ACCOUNTING_LABELS))
        return _(dflt_acct_debit_str);

    auto result = gnc_acct_debit_strs.find(acct_type);
    if (result != gnc_acct_debit_strs.end())
        return _(result->second);
    else
        return _(dflt_acct_debit_str);
}

const char *gnc_account_get_credit_string (GNCAccountType acct_type)
{
    if (gnc_prefs_get_bool(GNC_PREFS_GROUP_GENERAL, GNC_PREF_ACCOUNTING_LABELS))
        return _(dflt_acct_credit_str);

    auto result = gnc_acct_credit_strs.find(acct_type);
    if (result != gnc_acct_credit_strs.end())
        return _(result->second);
    else
        return _(dflt_acct_credit_str);
}

/********************************************************************\
\********************************************************************/

gboolean
xaccAccountGetPlaceholder (const Account *acc)
{
    return get_kvp_boolean_path(acc, {"placeholder"});
}

void
xaccAccountSetPlaceholder (Account *acc, gboolean val)
{
    set_kvp_boolean_path(acc, {"placeholder"}, val);
}

gboolean
xaccAccountGetAppendText (const Account *acc)
{
    return get_kvp_boolean_path(acc, {"import-append-text"});
}

void
xaccAccountSetAppendText (Account *acc, gboolean val)
{
    set_kvp_boolean_path(acc, {"import-append-text"}, val);
}

gboolean
xaccAccountGetIsOpeningBalance (const Account *acc)
{
    g_return_val_if_fail (GNC_IS_ACCOUNT(acc), false);
    if (GET_PRIVATE(acc)->type != ACCT_TYPE_EQUITY)
        return false;

    return !g_strcmp0 (get_kvp_string_path (acc, {"equity-type"}), "opening-balance");
}

void
xaccAccountSetIsOpeningBalance (Account *acc, gboolean val)
{
    g_return_if_fail (GNC_IS_ACCOUNT(acc));
    if (GET_PRIVATE(acc)->type != ACCT_TYPE_EQUITY)
        return;
    set_kvp_string_path(acc, {"equity-type"}, val ? "opening-balance" : nullptr);
}

GNCPlaceholderType
xaccAccountGetDescendantPlaceholder (const Account *acc)
{
    g_return_val_if_fail(GNC_IS_ACCOUNT(acc), PLACEHOLDER_NONE);
    if (xaccAccountGetPlaceholder(acc)) return PLACEHOLDER_THIS;

    return gnc_account_foreach_descendant_until (acc, (AccountCb2)xaccAccountGetPlaceholder, nullptr)
        ? PLACEHOLDER_CHILD : PLACEHOLDER_NONE;
}

/********************************************************************\
 \********************************************************************/

gboolean
xaccAccountGetAutoInterest (const Account *acc)
{
    return get_kvp_boolean_path (acc, {KEY_RECONCILE_INFO, "auto-interest-transfer"});
}

void
xaccAccountSetAutoInterest (Account *acc, gboolean val)
{
    set_kvp_boolean_path (acc, {KEY_RECONCILE_INFO, "auto-interest-transfer"}, val);
}

/********************************************************************\
\********************************************************************/

gboolean
xaccAccountGetHidden (const Account *acc)
{
    return get_kvp_boolean_path (acc, {"hidden"});
}

void
xaccAccountSetHidden (Account *acc, gboolean val)
{
    set_kvp_boolean_path (acc, {"hidden"}, val);
}

gboolean
xaccAccountIsHidden (const Account *acc)
{
    AccountPrivate *priv;

    g_return_val_if_fail(GNC_IS_ACCOUNT(acc), FALSE);

    if (xaccAccountGetHidden(acc))
        return TRUE;
    priv = GET_PRIVATE(acc);
    while ((acc = priv->parent) != nullptr)
    {
        priv = GET_PRIVATE(acc);
        if (xaccAccountGetHidden(acc))
            return TRUE;
    }
    return FALSE;
}

/********************************************************************\
\********************************************************************/

gboolean
xaccAccountHasAncestor (const Account *acc, const Account * ancestor)
{
    const Account *parent;

    g_return_val_if_fail(GNC_IS_ACCOUNT(acc), FALSE);
    g_return_val_if_fail(GNC_IS_ACCOUNT(ancestor), FALSE);

    parent = acc;
    while (parent && parent != ancestor)
        parent = GET_PRIVATE(parent)->parent;

    return (parent == ancestor);
}

/********************************************************************\
\********************************************************************/

/* You must edit the functions in this block in tandem.  KEEP THEM IN
   SYNC! */

#define GNC_RETURN_ENUM_AS_STRING(x) case (ACCT_TYPE_ ## x): return #x;

const char *
xaccAccountTypeEnumAsString(GNCAccountType type)
{
    switch (type)
    {
        GNC_RETURN_ENUM_AS_STRING(NONE);
        GNC_RETURN_ENUM_AS_STRING(BANK);
        GNC_RETURN_ENUM_AS_STRING(CASH);
        GNC_RETURN_ENUM_AS_STRING(CREDIT);
        GNC_RETURN_ENUM_AS_STRING(ASSET);
        GNC_RETURN_ENUM_AS_STRING(LIABILITY);
        GNC_RETURN_ENUM_AS_STRING(STOCK);
        GNC_RETURN_ENUM_AS_STRING(MUTUAL);
        GNC_RETURN_ENUM_AS_STRING(CURRENCY);
        GNC_RETURN_ENUM_AS_STRING(INCOME);
        GNC_RETURN_ENUM_AS_STRING(EXPENSE);
        GNC_RETURN_ENUM_AS_STRING(EQUITY);
        GNC_RETURN_ENUM_AS_STRING(RECEIVABLE);
        GNC_RETURN_ENUM_AS_STRING(PAYABLE);
        GNC_RETURN_ENUM_AS_STRING(ROOT);
        GNC_RETURN_ENUM_AS_STRING(TRADING);
        GNC_RETURN_ENUM_AS_STRING(CHECKING);
        GNC_RETURN_ENUM_AS_STRING(SAVINGS);
        GNC_RETURN_ENUM_AS_STRING(MONEYMRKT);
        GNC_RETURN_ENUM_AS_STRING(CREDITLINE);
    default:
        PERR ("asked to translate unknown account type %d.\n", type);
        break;
    }
    return(nullptr);
}

#undef GNC_RETURN_ENUM_AS_STRING

#define GNC_RETURN_ON_MATCH(x) \
  if(g_strcmp0(#x, (str)) == 0) { *type = ACCT_TYPE_ ## x; return(TRUE); }

gboolean
xaccAccountStringToType(const char* str, GNCAccountType *type)
{

    GNC_RETURN_ON_MATCH(NONE);
    GNC_RETURN_ON_MATCH(BANK);
    GNC_RETURN_ON_MATCH(CASH);
    GNC_RETURN_ON_MATCH(CREDIT);
    GNC_RETURN_ON_MATCH(ASSET);
    GNC_RETURN_ON_MATCH(LIABILITY);
    GNC_RETURN_ON_MATCH(STOCK);
    GNC_RETURN_ON_MATCH(MUTUAL);
    GNC_RETURN_ON_MATCH(CURRENCY);
    GNC_RETURN_ON_MATCH(INCOME);
    GNC_RETURN_ON_MATCH(EXPENSE);
    GNC_RETURN_ON_MATCH(EQUITY);
    GNC_RETURN_ON_MATCH(RECEIVABLE);
    GNC_RETURN_ON_MATCH(PAYABLE);
    GNC_RETURN_ON_MATCH(ROOT);
    GNC_RETURN_ON_MATCH(TRADING);
    GNC_RETURN_ON_MATCH(CHECKING);
    GNC_RETURN_ON_MATCH(SAVINGS);
    GNC_RETURN_ON_MATCH(MONEYMRKT);
    GNC_RETURN_ON_MATCH(CREDITLINE);

    PERR("asked to translate unknown account type string %s.\n",
         str ? str : "(null)");

    return(FALSE);
}

#undef GNC_RETURN_ON_MATCH

/* impedance mismatch is a source of loss */
GNCAccountType
xaccAccountStringToEnum(const char* str)
{
    GNCAccountType type;
    gboolean rc;
    rc = xaccAccountStringToType(str, &type);
    if (FALSE == rc) return ACCT_TYPE_INVALID;
    return type;
}

/********************************************************************\
\********************************************************************/

static char const *
account_type_name[NUM_ACCOUNT_TYPES] =
{
    N_("Bank"),
    N_("Cash"),
    N_("Asset"),
    N_("Credit Card"),
    N_("Liability"),
    N_("Stock"),
    N_("Mutual Fund"),
    N_("Currency"),
    N_("Income"),
    N_("Expense"),
    N_("Equity"),
    N_("A/Receivable"),
    N_("A/Payable"),
    N_("Root"),
    N_("Trading")
    /*
      N_("Checking"),
      N_("Savings"),
      N_("Money Market"),
      N_("Credit Line")
    */
};

const char *
xaccAccountGetTypeStr(GNCAccountType type)
{
    if (type < 0 || NUM_ACCOUNT_TYPES <= type ) return "";
    return _(account_type_name [type]);
}

/********************************************************************\
\********************************************************************/

guint32
xaccAccountTypesCompatibleWith (GNCAccountType type)
{
    switch (type)
    {
        case ACCT_TYPE_BANK:
        case ACCT_TYPE_CASH:
        case ACCT_TYPE_ASSET:
        case ACCT_TYPE_CREDIT:
        case ACCT_TYPE_LIABILITY:
        case ACCT_TYPE_INCOME:
        case ACCT_TYPE_EXPENSE:
        case ACCT_TYPE_EQUITY:
            return
            (1 << ACCT_TYPE_BANK)       |
            (1 << ACCT_TYPE_CASH)       |
            (1 << ACCT_TYPE_ASSET)      |
            (1 << ACCT_TYPE_CREDIT)     |
            (1 << ACCT_TYPE_LIABILITY)  |
            (1 << ACCT_TYPE_INCOME)     |
            (1 << ACCT_TYPE_EXPENSE)    |
            (1 << ACCT_TYPE_EQUITY);
        case ACCT_TYPE_STOCK:
        case ACCT_TYPE_MUTUAL:
        case ACCT_TYPE_CURRENCY:
            return
            (1 << ACCT_TYPE_STOCK)      |
            (1 << ACCT_TYPE_MUTUAL)     |
            (1 << ACCT_TYPE_CURRENCY);
        case ACCT_TYPE_RECEIVABLE:
            return (1 << ACCT_TYPE_RECEIVABLE);
        case ACCT_TYPE_PAYABLE:
            return (1 << ACCT_TYPE_PAYABLE);
        case ACCT_TYPE_TRADING:
            return (1 << ACCT_TYPE_TRADING);
        default:
            PERR("bad account type: %d", type);
            return 0;
    }
}
guint32
xaccParentAccountTypesCompatibleWith (GNCAccountType type)
{
    switch (type)
    {
    case ACCT_TYPE_BANK:
    case ACCT_TYPE_CASH:
    case ACCT_TYPE_ASSET:
    case ACCT_TYPE_STOCK:
    case ACCT_TYPE_MUTUAL:
    case ACCT_TYPE_CURRENCY:
    case ACCT_TYPE_CREDIT:
    case ACCT_TYPE_LIABILITY:
    case ACCT_TYPE_RECEIVABLE:
    case ACCT_TYPE_PAYABLE:
        return
            (1 << ACCT_TYPE_BANK)       |
            (1 << ACCT_TYPE_CASH)       |
            (1 << ACCT_TYPE_ASSET)      |
            (1 << ACCT_TYPE_STOCK)      |
            (1 << ACCT_TYPE_MUTUAL)     |
            (1 << ACCT_TYPE_CURRENCY)   |
            (1 << ACCT_TYPE_CREDIT)     |
            (1 << ACCT_TYPE_LIABILITY)  |
            (1 << ACCT_TYPE_RECEIVABLE) |
            (1 << ACCT_TYPE_PAYABLE)    |
            (1 << ACCT_TYPE_ROOT);
    case ACCT_TYPE_INCOME:
    case ACCT_TYPE_EXPENSE:
        return
            (1 << ACCT_TYPE_INCOME)     |
            (1 << ACCT_TYPE_EXPENSE)    |
            (1 << ACCT_TYPE_ROOT);
    case ACCT_TYPE_EQUITY:
        return
            (1 << ACCT_TYPE_EQUITY)     |
            (1 << ACCT_TYPE_ROOT);
    case ACCT_TYPE_TRADING:
        return
            (1 << ACCT_TYPE_TRADING)    |
            (1 << ACCT_TYPE_ROOT);
    default:
        PERR("bad account type: %d", type);
        return 0;
    }
}

gboolean
xaccAccountTypesCompatible (GNCAccountType parent_type,
                            GNCAccountType child_type)
{
    /* ACCT_TYPE_NONE isn't compatible with anything, even ACCT_TYPE_NONE. */
    if (parent_type == ACCT_TYPE_NONE || child_type == ACCT_TYPE_NONE)
        return FALSE;

    /* ACCT_TYPE_ROOT can't have a parent account, and asking will raise
     * an error. */
    if (child_type == ACCT_TYPE_ROOT)
        return FALSE;

    return ((xaccParentAccountTypesCompatibleWith (child_type) &
             (1 << parent_type))
            != 0);
}

guint32
xaccAccountTypesValid(void)
{
    guint32 mask = (1 << NUM_ACCOUNT_TYPES) - 1;
    mask &= ~((1 << ACCT_TYPE_CURRENCY) |  /* DEPRECATED */
              (1 << ACCT_TYPE_ROOT));      /* ROOT */

    return mask;
}

gboolean xaccAccountIsAssetLiabType(GNCAccountType t)
{
    switch (t)
    {
    case ACCT_TYPE_RECEIVABLE:
    case ACCT_TYPE_PAYABLE:
        return FALSE;
    default:
        return (xaccAccountTypesCompatible(ACCT_TYPE_ASSET, t)
                || xaccAccountTypesCompatible(ACCT_TYPE_LIABILITY, t));
    }
}

GNCAccountType
xaccAccountTypeGetFundamental (GNCAccountType t)
{
    switch (t)
    {
        case ACCT_TYPE_BANK:
        case ACCT_TYPE_STOCK:
        case ACCT_TYPE_MONEYMRKT:
        case ACCT_TYPE_CHECKING:
        case ACCT_TYPE_SAVINGS:
        case ACCT_TYPE_MUTUAL:
        case ACCT_TYPE_CURRENCY:
        case ACCT_TYPE_CASH:
        case ACCT_TYPE_ASSET:
        case ACCT_TYPE_RECEIVABLE:
            return ACCT_TYPE_ASSET;
        case ACCT_TYPE_CREDIT:
        case ACCT_TYPE_LIABILITY:
        case ACCT_TYPE_PAYABLE:
        case ACCT_TYPE_CREDITLINE:
            return ACCT_TYPE_LIABILITY;
        case ACCT_TYPE_INCOME:
            return ACCT_TYPE_INCOME;
        case ACCT_TYPE_EXPENSE:
            return ACCT_TYPE_EXPENSE;
        case ACCT_TYPE_EQUITY:
            return ACCT_TYPE_EQUITY;
        case ACCT_TYPE_TRADING:
        default:
            return ACCT_TYPE_NONE;
    }
}

gboolean xaccAccountIsAPARType(GNCAccountType t)
{
    switch (t)
    {
    case ACCT_TYPE_RECEIVABLE:
    case ACCT_TYPE_PAYABLE:
        return TRUE;
    default:
        return FALSE;
    }
}

gboolean xaccAccountIsEquityType(GNCAccountType t)
{
    switch (t)
    {
    case ACCT_TYPE_EQUITY:
        return TRUE;
    default:
        return FALSE;
    }
}

gboolean
xaccAccountIsPriced(const Account *acc)
{
    AccountPrivate *priv;

    g_return_val_if_fail(GNC_IS_ACCOUNT(acc), FALSE);

    priv = GET_PRIVATE(acc);
    return (priv->type == ACCT_TYPE_STOCK || priv->type == ACCT_TYPE_MUTUAL ||
            priv->type == ACCT_TYPE_CURRENCY);
}

/********************************************************************\
\********************************************************************/

gboolean
xaccAccountGetReconcileLastDate (const Account *acc, time64 *last_date)
{
    gboolean retval = FALSE;
    auto date = get_kvp_int64_path (acc, {KEY_RECONCILE_INFO, "last-date"});

    if (date)
    {
        if (last_date)
            *last_date = *date;
        retval = TRUE;
    }
    return retval;
}

/********************************************************************\
\********************************************************************/

void
xaccAccountSetReconcileLastDate (Account *acc, time64 last_date)
{
    set_kvp_int64_path (acc, {KEY_RECONCILE_INFO, "last-date"}, last_date);
}

/********************************************************************\
\********************************************************************/

gboolean
xaccAccountGetReconcileLastInterval (const Account *acc,
                                     int *months, int *days)
{
    if (!acc) return FALSE;
    auto m{get_kvp_int64_path (acc, {KEY_RECONCILE_INFO, "last-interval", "months"})};
    auto d{get_kvp_int64_path (acc, {KEY_RECONCILE_INFO, "last-interval", "days"})};
    if (m && d)
    {
        if (months)
            *months = *m;
        if (days)
            *days = *d;
        return true;
    }
    return false;
}

/********************************************************************\
\********************************************************************/

void
xaccAccountSetReconcileLastInterval (Account *acc, int months, int days)
{
    set_kvp_int64_path (acc, {KEY_RECONCILE_INFO, "last-interval", "months"}, months);
    set_kvp_int64_path (acc, {KEY_RECONCILE_INFO, "last-interval", "days"}, days);
}

/********************************************************************\
\********************************************************************/

gboolean
xaccAccountGetReconcilePostponeDate (const Account *acc, time64 *postpone_date)
{
    if (auto date = get_kvp_int64_path (acc, {KEY_RECONCILE_INFO, KEY_POSTPONE, "date"}))
    {
        if (postpone_date)
            *postpone_date = *date;
        return true;
    }
    return false;
}

/********************************************************************\
\********************************************************************/

void
xaccAccountSetReconcilePostponeDate (Account *acc, time64 postpone_date)
{
    set_kvp_int64_path (acc, {KEY_RECONCILE_INFO, KEY_POSTPONE, "date"}, postpone_date);
}

/********************************************************************\
\********************************************************************/

gboolean
xaccAccountGetReconcilePostponeBalance (const Account *acc,
                                        gnc_numeric *balance)
{
    if (auto bal = get_kvp_gnc_numeric_path (acc, {KEY_RECONCILE_INFO, KEY_POSTPONE, "balance"}))
    {
        if (balance)
            *balance = *bal;
        return true;
    }
    return false;
}

/********************************************************************\
\********************************************************************/

void
xaccAccountSetReconcilePostponeBalance (Account *acc, gnc_numeric balance)
{
    set_kvp_gnc_numeric_path (acc, {KEY_RECONCILE_INFO, KEY_POSTPONE, "balance"}, balance);
}

/********************************************************************\

\********************************************************************/

void
xaccAccountClearReconcilePostpone (Account *acc)
{
    set_kvp_gnc_numeric_path (acc, {KEY_RECONCILE_INFO, KEY_POSTPONE, "balance"}, {});
}

/********************************************************************\
\********************************************************************/

const char *
xaccAccountGetLastNum (const Account *acc)
{
    return get_kvp_string_path (acc, {"last-num"});
}

/********************************************************************\
\********************************************************************/

void
xaccAccountSetLastNum (Account *acc, const char *num)
{
    set_kvp_string_path (acc, {"last-num"}, num);
}


/********************************************************************\
\********************************************************************/

static bool
get_balance_limit (const Account* acc, const std::string& key, gnc_numeric* balance)
{
    auto limit = get_kvp_gnc_numeric_path (acc, {KEY_BALANCE_LIMIT, key});
    if (limit)
        *balance = gnc_numeric_create (limit->num, limit->denom);
    return limit.has_value();
}

static void
set_balance_limit (Account *acc, const std::string& key, std::optional<gnc_numeric> balance)
{
    if (balance && gnc_numeric_check (*balance))
        return;
    set_kvp_gnc_numeric_path (acc, {KEY_BALANCE_LIMIT, key}, balance);
}

gboolean
xaccAccountGetHigherBalanceLimit (const Account *acc,
                                  gnc_numeric *balance)
{
    return get_balance_limit (acc, KEY_BALANCE_HIGHER_LIMIT_VALUE, balance);
}

gboolean
xaccAccountGetLowerBalanceLimit (const Account *acc,
                                 gnc_numeric *balance)
{
    return get_balance_limit (acc, KEY_BALANCE_LOWER_LIMIT_VALUE, balance);
}

void
xaccAccountSetHigherBalanceLimit (Account *acc, gnc_numeric balance)
{
    set_balance_limit (acc, KEY_BALANCE_HIGHER_LIMIT_VALUE, balance);
}

void
xaccAccountSetLowerBalanceLimit (Account *acc, gnc_numeric balance)
{
    set_balance_limit (acc, KEY_BALANCE_LOWER_LIMIT_VALUE, balance);
}

void
xaccAccountClearHigherBalanceLimit (Account *acc)
{
    set_balance_limit (acc, KEY_BALANCE_HIGHER_LIMIT_VALUE, {});
}

void
xaccAccountClearLowerBalanceLimit (Account *acc)
{
    set_balance_limit (acc, KEY_BALANCE_LOWER_LIMIT_VALUE, {});
}

gboolean
xaccAccountGetIncludeSubAccountBalances (const Account *acc)
{
    return get_kvp_boolean_path (acc, {KEY_BALANCE_LIMIT, KEY_BALANCE_INCLUDE_SUB_ACCTS});
}

void
xaccAccountSetIncludeSubAccountBalances (Account *acc, gboolean inc_sub)
{
    set_kvp_boolean_path (acc, {KEY_BALANCE_LIMIT, KEY_BALANCE_INCLUDE_SUB_ACCTS}, inc_sub);
}

/********************************************************************\
\********************************************************************/

static Account *
GetOrMakeOrphanAccount (Account *root, gnc_commodity * currency)
{
    char * accname;
    Account * acc;

    g_return_val_if_fail (root, nullptr);

    /* build the account name */
    if (!currency)
    {
        PERR ("No currency specified!");
        return nullptr;
    }

    accname = g_strconcat (_("Orphaned Gains"), "-",
                           gnc_commodity_get_mnemonic (currency), nullptr);

    /* See if we've got one of these going already ... */
    acc = gnc_account_lookup_by_name(root, accname);

    if (acc == nullptr)
    {
        /* Guess not. We'll have to build one. */
        acc = xaccMallocAccount (gnc_account_get_book(root));
        xaccAccountBeginEdit (acc);
        xaccAccountSetName (acc, accname);
        xaccAccountSetCommodity (acc, currency);
        xaccAccountSetType (acc, ACCT_TYPE_INCOME);
        xaccAccountSetDescription (acc, _("Realized Gain/Loss"));
        xaccAccountSetNotes (acc,
                             _("Realized Gains or Losses from "
                               "Commodity or Trading Accounts "
                               "that haven't been recorded elsewhere."));

        /* Hang the account off the root. */
        gnc_account_append_child (root, acc);
        xaccAccountCommitEdit (acc);
    }

    g_free (accname);

    return acc;
}

Account *
xaccAccountGainsAccount (Account *acc, gnc_commodity *curr)
{
    Path path {KEY_LOT_MGMT, "gains-acct", gnc_commodity_get_unique_name (curr)};
    auto gains_account = get_kvp_account_path (acc, path);

    if (gains_account == nullptr) /* No gains account for this currency */
    {
        gains_account = GetOrMakeOrphanAccount (gnc_account_get_root (acc), curr);
        set_kvp_account_path (acc, path, gains_account);
    }

    return gains_account;
}

/********************************************************************\
\********************************************************************/

void
dxaccAccountSetPriceSrc(Account *acc, const char *src)
{
    if (!acc) return;

    if (xaccAccountIsPriced(acc))
        set_kvp_string_path (acc, {"old-price-source"}, src);
}

/********************************************************************\
\********************************************************************/

const char*
dxaccAccountGetPriceSrc(const Account *acc)
{
    static char *source = nullptr;
    if (!acc) return nullptr;

    if (!xaccAccountIsPriced(acc)) return nullptr;

    g_free (source);

    return get_kvp_string_path (acc, {"old-price-source"});
}

/********************************************************************\
\********************************************************************/

void
dxaccAccountSetQuoteTZ(Account *acc, const char *tz)
{
    if (!acc) return;
    if (!xaccAccountIsPriced(acc)) return;
    set_kvp_string_path (acc, {"old-quote-tz"}, tz);
}

/********************************************************************\
\********************************************************************/

const char*
dxaccAccountGetQuoteTZ(const Account *acc)
{
    if (!acc) return nullptr;
    if (!xaccAccountIsPriced(acc)) return nullptr;
    return get_kvp_string_path (acc, {"old-quote-tz"});
}

/********************************************************************\
\********************************************************************/

void
xaccAccountSetReconcileChildrenStatus(Account *acc, gboolean status)
{
    /* Would have been nice to use G_TYPE_BOOLEAN, but the other
     * boolean kvps save the value as "true" or "false" and that would
     * be file-incompatible with this.
     */
    set_kvp_int64_path (acc, {KEY_RECONCILE_INFO, KEY_INCLUDE_CHILDREN}, status);
}

/********************************************************************\
\********************************************************************/

gboolean
xaccAccountGetReconcileChildrenStatus(const Account *acc)
{
    /* access the account's kvp-data for status and return that, if no value
     * is found then we can assume not to include the children, that being
     * the default behaviour
     */
    return get_kvp_boolean_path (acc, {KEY_RECONCILE_INFO, KEY_INCLUDE_CHILDREN});
}

/********************************************************************\
\********************************************************************/

Split *
xaccAccountFindSplitByDesc(const Account *acc, const char *description)
{
    auto has_description = [description](const Split* s) -> bool
    { return !g_strcmp0 (description, xaccTransGetDescription (xaccSplitGetParent (s))); };
    return gnc_account_find_split (acc, has_description, true);
}

/* This routine is for finding a matching transaction in an account by
 * matching on the description field. [CAS: The rest of this comment
 * seems to belong somewhere else.] This routine is used for
 * auto-filling in registers with a default leading account. The
 * dest_trans is a transaction used for currency checking. */
Transaction *
xaccAccountFindTransByDesc(const Account *acc, const char *description)
{
    auto split = xaccAccountFindSplitByDesc (acc, description);
    return split ? xaccSplitGetParent (split) : nullptr;
}

/* ================================================================ */
/* Concatenation, Merging functions                                */

void
gnc_account_join_children (Account *to_parent, Account *from_parent)
{

    /* errors */
    g_return_if_fail(GNC_IS_ACCOUNT(to_parent));
    g_return_if_fail(GNC_IS_ACCOUNT(from_parent));

    /* optimizations */
    auto from_priv = GET_PRIVATE(from_parent);
    if (from_priv->children.empty())
        return;

    ENTER (" ");
    auto children = from_priv->children;
    for (auto child : children)
        gnc_account_append_child(to_parent, child);
    LEAVE (" ");
}
/********************************************************************\
\********************************************************************/

void
gnc_account_merge_children (Account *parent)
{
    g_return_if_fail(GNC_IS_ACCOUNT(parent));

    auto ppriv = GET_PRIVATE(parent);
    for (auto it_a = ppriv->children.begin(); it_a != ppriv->children.end(); it_a++)
    {
        auto acc_a = *it_a;
        auto priv_a = GET_PRIVATE(acc_a);
        for (auto it_b = std::next(it_a); it_b != ppriv->children.end(); it_b++)
        {
            auto acc_b = *it_b;
            auto priv_b = GET_PRIVATE(acc_b);
            if (0 != null_strcmp(priv_a->accountName, priv_b->accountName))
                continue;
            if (0 != null_strcmp(priv_a->accountCode, priv_b->accountCode))
                continue;
            if (0 != null_strcmp(priv_a->description, priv_b->description))
                continue;
            if (0 != null_strcmp(xaccAccountGetColor(acc_a),
                                 xaccAccountGetColor(acc_b)))
                continue;
            if (!gnc_commodity_equiv(priv_a->commodity, priv_b->commodity))
                continue;
            if (0 != null_strcmp(xaccAccountGetNotes(acc_a),
                                 xaccAccountGetNotes(acc_b)))
                continue;
            if (priv_a->type != priv_b->type)
                continue;

            /* consolidate children */
            if (!priv_b->children.empty())
            {
                auto work = priv_b->children;
                for (auto w : work)
                    gnc_account_append_child (acc_a, w);

                qof_event_gen (&acc_a->inst, QOF_EVENT_MODIFY, nullptr);
                qof_event_gen (&acc_b->inst, QOF_EVENT_MODIFY, nullptr);
            }

            /* recurse to do the children's children */
            gnc_account_merge_children (acc_a);

            /* consolidate transactions */
            while (!priv_b->splits.empty())
                xaccSplitSetAccount (priv_b->splits.front(), acc_a);

            /* move back one before removal. next iteration around the loop
             * will get the node after node_b */
            it_b--;

            /* The destroy function will remove from list -- node_a is ok,
             * it's before node_b */
            xaccAccountBeginEdit (acc_b);
            xaccAccountDestroy (acc_b);
        }
    }
}

/* ================================================================ */
/* Transaction Traversal functions                                  */


static void
xaccSplitsBeginStagedTransactionTraversals (SplitsVec& splits)
{
    for (auto s : splits)
    {
        Transaction *trans = s->parent;

        if (trans)
            trans->marker = 0;
    }
}

/* original function */
void
xaccAccountBeginStagedTransactionTraversals (const Account *account)
{
    if (!account)
        return;
    xaccSplitsBeginStagedTransactionTraversals(GET_PRIVATE (account)->splits);
}

gboolean
xaccTransactionTraverse (Transaction *trans, int stage)
{
    if (trans == nullptr) return FALSE;

    if (trans->marker < stage)
    {
        trans->marker = stage;
        return TRUE;
    }

    return FALSE;
}

/* Replacement for xaccGroupBeginStagedTransactionTraversals */
void
gnc_account_tree_begin_staged_transaction_traversals (Account *account)
{
    auto do_one_account = [](auto acc)
    { gnc_account_foreach_split (acc, [](auto s){ s->parent->marker = 0; }, false); };
    gnc_account_foreach_descendant (account, do_one_account);
}

int
xaccAccountStagedTransactionTraversal (const Account *acc,
                                       unsigned int stage,
                                       TransactionCallback thunk,
                                       void *cb_data)
{
    if (!acc) return 0;

    // iterate on copy of splits. some callers modify the splitsvec.
    auto splits = GET_PRIVATE(acc)->splits;
    for (auto s : splits)
    {
        auto trans = s->parent;
        if (trans && (trans->marker < stage))
        {
            trans->marker = stage;
            if (thunk)
            {
                auto retval = thunk(trans, cb_data);
                if (retval) return retval;
            }
        }
    }

    return 0;
}

int
gnc_account_tree_staged_transaction_traversal (const Account *acc,
        unsigned int stage,
        TransactionCallback thunk,
        void *cb_data)
{
    const AccountPrivate *priv;
    Transaction *trans;
    int retval;

    if (!acc) return 0;

    /* depth first traversal */
    priv = GET_PRIVATE(acc);
    for (auto acc_p : priv->children)
    {
        retval = gnc_account_tree_staged_transaction_traversal(acc_p, stage, thunk, cb_data);
        if (retval) return retval;
    }

    /* Now this account */
    for (auto s : priv->splits)
    {
        trans = s->parent;
        if (trans && (trans->marker < stage))
        {
            trans->marker = stage;
            if (thunk)
            {
                retval = thunk(trans, cb_data);
                if (retval) return retval;
            }
        }
    }

    return 0;
}

/********************************************************************\
\********************************************************************/

int
xaccAccountTreeForEachTransaction (Account *acc,
                                   int (*proc)(Transaction *t, void *data),
                                   void *data)
{
    if (!acc || !proc) return 0;

    gnc_account_tree_begin_staged_transaction_traversals (acc);
    return gnc_account_tree_staged_transaction_traversal (acc, 42, proc, data);
}


gint
xaccAccountForEachTransaction(const Account *acc, TransactionCallback proc,
                              void *data)
{
    if (!acc || !proc) return 0;
    xaccAccountBeginStagedTransactionTraversals (acc);
    return xaccAccountStagedTransactionTraversal(acc, 42, proc, data);
}

/* ================================================================ */
/* The following functions are used by
 * src/import-export/import-backend.c to manipulate the contra-account
 * matching data. See src/import-export/import-backend.c for explanations.
 */

#define IMAP_FRAME              "import-map"
#define IMAP_FRAME_BAYES        "import-map-bayes"

/* Look up an Account in the map */
Account*
gnc_account_imap_find_account (Account *acc,
                               const char *category,
                               const char *key)
{
    if (!acc || !key) return nullptr;
    std::vector<std::string> path {IMAP_FRAME};
    if (category)
        path.push_back (category);
    path.push_back (key);
    return get_kvp_account_path (acc, path);
}

Account*
gnc_account_imap_find_any (QofBook *book, const char* category, const char *key)
{
    Account *account = nullptr;

    /* Get list of Accounts */
    auto root = gnc_book_get_root_account (book);
    auto accts = gnc_account_get_descendants_sorted (root);

    /* Go through list of accounts */
    for (auto ptr = accts; ptr; ptr = g_list_next (ptr))
    {
        auto tmp_acc = static_cast<Account*> (ptr->data);

        if (gnc_account_imap_find_account (tmp_acc, category, key))
        {
            account = tmp_acc;
            break;
        }
    }
    g_list_free (accts);

return account;
}

/* Store an Account in the map */
void
gnc_account_imap_add_account (Account *acc,
                              const char *category,
                              const char *key,
                              Account *added_acc)
{
    if (!acc || !key || !added_acc || !*key) return;

    auto path = category ? Path{IMAP_FRAME, category, key} : Path{IMAP_FRAME, key};

    set_kvp_account_path (acc, path, added_acc);
}

/* Remove a reference to an Account in the map */
void
gnc_account_imap_delete_account (Account *acc,
                                 const char *category,
                                 const char *key)
{
    if (!acc || !key) return;

    auto path = category ? Path{IMAP_FRAME, category, key} : Path{IMAP_FRAME, key};
    if (qof_instance_has_path_slot (QOF_INSTANCE (acc), path))
    {
        qof_instance_slot_path_delete (QOF_INSTANCE (acc), path);
        if (category)
            qof_instance_slot_path_delete_if_empty (QOF_INSTANCE (acc), {IMAP_FRAME, category});
        qof_instance_slot_path_delete_if_empty (QOF_INSTANCE (acc), {IMAP_FRAME});
    }
    qof_instance_set_dirty (QOF_INSTANCE (acc));
}

/*--------------------------------------------------------------------------
 Below here is the bayes transaction to account matching system
--------------------------------------------------------------------------*/


/** intermediate values used to calculate the bayes probability of a given account
  where p(AB) = (a*b)/[a*b + (1-a)(1-b)], product is (a*b),
  product_difference is (1-a) * (1-b)
 */
struct AccountProbability
{
    double product; /* product of probabilities */
    double product_difference; /* product of (1-probabilities) */
};

struct AccountTokenCount
{
    std::string account_guid;
    int64_t token_count; /** occurrences of a given token for this account_guid */
};

/** total_count and the token_count for a given account let us calculate the
 * probability of a given account with any single token
 */
struct TokenAccountsInfo
{
    std::vector<AccountTokenCount> accounts;
    int64_t total_count;
};

/** holds an account guid and its corresponding integer probability
  the integer probability is some factor of 10
 */
struct AccountInfo
{
    std::string account_guid;
    int32_t probability;
};

static void
build_token_info(char const * suffix, KvpValue * value, TokenAccountsInfo & tokenInfo)
{
    if (strlen(suffix) == GUID_ENCODING_LENGTH)
    {
        tokenInfo.total_count += value->get<int64_t>();
        /*By convention, the key ends with the account GUID.*/
        tokenInfo.accounts.emplace_back(AccountTokenCount{std::string{suffix}, value->get<int64_t>()});
    }
}

/** We scale the probability values by probability_factor.
  ie. with probability_factor of 100000, 10% would be
  0.10 * 100000 = 10000 */
static constexpr int probability_factor = 100000;

static FinalProbabilityVec
build_probabilities(ProbabilityVec const & first_pass)
{
    FinalProbabilityVec ret;
    for (auto const & first_pass_prob : first_pass)
    {
        auto const & account_probability = first_pass_prob.second;
        /* P(AB) = A*B / [A*B + (1-A)*(1-B)]
         * NOTE: so we only keep track of a running product(A*B*C...)
         * and product difference ((1-A)(1-B)...)
         */
        int32_t probability = (account_probability.product /
                (account_probability.product + account_probability.product_difference)) * probability_factor;
        ret.push_back({first_pass_prob.first, probability});
    }
    return ret;
}

static AccountInfo
highest_probability(FinalProbabilityVec const & probabilities)
{
    AccountInfo ret {"", std::numeric_limits<int32_t>::min()};
    for (auto const & prob : probabilities)
        if (prob.second > ret.probability)
            ret = AccountInfo {prob.first, prob.second};
    return ret;
}

static ProbabilityVec
get_first_pass_probabilities(Account* acc, GList * tokens)
{
    ProbabilityVec ret;
    /* find the probability for each account that contains any of the tokens
     * in the input tokens list. */
    for (auto current_token = tokens; current_token; current_token = current_token->next)
    {
        TokenAccountsInfo tokenInfo{};
        auto path = std::string{IMAP_FRAME_BAYES "/"} + static_cast <char const *> (current_token->data) + "/";
        qof_instance_foreach_slot_prefix (QOF_INSTANCE (acc), path, &build_token_info, tokenInfo);
        for (auto const & current_account_token : tokenInfo.accounts)
        {
            auto item = std::find_if(ret.begin(), ret.end(), [&current_account_token]
                (std::pair<std::string, AccountProbability> const & a) {
                    return current_account_token.account_guid == a.first;
                });
            if (item != ret.end())
            {/* This account is already in the map */
                item->second.product = ((double)current_account_token.token_count /
                                      (double)tokenInfo.total_count) * item->second.product;
                item->second.product_difference = ((double)1 - ((double)current_account_token.token_count /
                                              (double)tokenInfo.total_count)) * item->second.product_difference;
            }
            else
            {
                /* add a new entry */
                AccountProbability new_probability;
                new_probability.product = ((double)current_account_token.token_count /
                                      (double)tokenInfo.total_count);
                new_probability.product_difference = 1 - (new_probability.product);
                ret.push_back({current_account_token.account_guid, std::move(new_probability)});
            }
        } /* for all accounts in tokenInfo */
    }
    return ret;
}

static std::string
look_for_old_separator_descendants (Account *root, std::string const & full_name, const gchar *separator)
{
    GList *top_accounts, *ptr;
    gint   found_len = 0;
    gchar  found_sep;
    top_accounts = gnc_account_get_descendants (root);
    PINFO("Incoming full_name is '%s', current separator is '%s'", full_name.c_str (), separator);
    /* Go through list of top level accounts */
    for (ptr = top_accounts; ptr; ptr = g_list_next (ptr))
    {
        const gchar *name = xaccAccountGetName (static_cast <Account const *> (ptr->data));
        // we are looking for the longest top level account that matches
        if (g_str_has_prefix (full_name.c_str (), name))
        {
            gint name_len = strlen (name);
            const gchar old_sep = full_name[name_len];
            if (!g_ascii_isalnum (old_sep)) // test for non alpha numeric
            {
                if (name_len > found_len)
                {
                    found_sep = full_name[name_len];
                    found_len = name_len;
                }
            }
        }
    }
    g_list_free (top_accounts); // Free the List
    std::string new_name {full_name};
    if (found_len > 1)
        std::replace (new_name.begin (), new_name.end (), found_sep, *separator);
    PINFO ("Return full_name is '%s'", new_name.c_str ());
    return new_name;
}

static std::string
get_guid_from_account_name (Account * root, std::string const & name)
{
    auto map_account = gnc_account_lookup_by_full_name (root, name.c_str ());
    if (!map_account)
    {
        auto temp_account_name = look_for_old_separator_descendants (root, name,
             gnc_get_account_separator_string ());
        map_account = gnc_account_lookup_by_full_name (root, temp_account_name.c_str ());
    }
    auto temp_guid = gnc::GUID {*xaccAccountGetGUID (map_account)};
    return temp_guid.to_string ();
}

static FlatKvpEntry
convert_entry (KvpEntry entry, Account* root)
{
    /*We need to make a copy here.*/
    auto account_name = entry.first.back();
    if (!gnc::GUID::is_valid_guid (account_name))
    {
        /* Earlier version stored the account name in the import map, and
         * there were early beta versions of 2.7 that stored a GUID.
         * If there is no GUID, we assume it's an account name. */
        /* Take off the account name and replace it with the GUID */
        entry.first.pop_back();
        auto guid_str = get_guid_from_account_name (root, account_name);
        entry.first.emplace_back (guid_str);
    }
    std::string new_key {std::accumulate (entry.first.begin(), entry.first.end(), std::string {})};
    new_key = IMAP_FRAME_BAYES + new_key;
    return {new_key, entry.second};
}

static std::vector<FlatKvpEntry>
get_flat_imap (Account * acc)
{
    auto frame = qof_instance_get_slots (QOF_INSTANCE (acc));
    auto slot = frame->get_slot ({IMAP_FRAME_BAYES});
    if (!slot)
        return {};
    auto imap_frame = slot->get<KvpFrame*> ();
    auto flat_kvp = imap_frame->flatten_kvp ();
    auto root = gnc_account_get_root (acc);
    std::vector <FlatKvpEntry> ret;
    for (auto const & flat_entry : flat_kvp)
    {
        auto converted_entry = convert_entry (flat_entry, root);
        /*If the entry was invalid, we don't perpetuate it.*/
        if (converted_entry.first.size())
            ret.emplace_back (converted_entry);
    }
    return ret;
}

static bool
convert_imap_account_bayes_to_flat (Account *acc)
{
    auto frame = qof_instance_get_slots (QOF_INSTANCE (acc));
    if (!frame->get_keys().size())
        return false;
    auto flat_imap = get_flat_imap(acc);
    if (!flat_imap.size ())
        return false;
    xaccAccountBeginEdit(acc);
    frame->set({IMAP_FRAME_BAYES}, nullptr);
    std::for_each(flat_imap.begin(), flat_imap.end(),
                  [&frame] (FlatKvpEntry const & entry) {
                      frame->set({entry.first.c_str()}, entry.second);
                  });
    qof_instance_set_dirty (QOF_INSTANCE (acc));
    xaccAccountCommitEdit(acc);
    return true;
}

/*
 * Checks for import map data and converts them when found.
 */
static bool
imap_convert_bayes_to_flat (QofBook * book)
{
    auto root = gnc_book_get_root_account (book);
    auto accts = gnc_account_get_descendants_sorted (root);
    bool ret = false;
    for (auto ptr = accts; ptr; ptr = g_list_next (ptr))
    {
        Account *acc = static_cast <Account*> (ptr->data);
        if (convert_imap_account_bayes_to_flat (acc))
        {
            ret = true;
            gnc_features_set_used (book, GNC_FEATURE_GUID_FLAT_BAYESIAN);
        }
    }
    g_list_free (accts);
    return ret;
}

void
gnc_account_reset_convert_bayes_to_flat (void)
{
    imap_convert_bayes_to_flat_run = false;
}

/*
 * Here we check to see the state of import map data.
 *
 * If the GUID_FLAT_BAYESIAN feature flag is set, everything
 * should be fine.
 *
 * If it is not set, there are two possibilities: import data
 * are present from a previous version or not. If they are,
 * they are converted, and the feature flag set. If there are
 * no previous data, nothing is done.
 */
static void
check_import_map_data (QofBook *book)
{
    if (gnc_features_check_used (book, GNC_FEATURE_GUID_FLAT_BAYESIAN) ||
        imap_convert_bayes_to_flat_run)
        return;

    /* This function will set GNC_FEATURE_GUID_FLAT_BAYESIAN if necessary.*/
    imap_convert_bayes_to_flat (book);
    imap_convert_bayes_to_flat_run = true;
}

static constexpr double threshold = .90 * probability_factor; /* 90% */

/** Look up an Account in the map */
Account*
gnc_account_imap_find_account_bayes (Account *acc, GList *tokens)
{
    if (!acc)
        return nullptr;
    auto book = gnc_account_get_book(acc);
    check_import_map_data (book);
    auto first_pass = get_first_pass_probabilities(acc, tokens);
    if (!first_pass.size())
        return nullptr;
    auto final_probabilities = build_probabilities(first_pass);
    if (!final_probabilities.size())
        return nullptr;
    auto best = highest_probability(final_probabilities);
    if (best.account_guid == "")
        return nullptr;
    if (best.probability < threshold)
        return nullptr;
    gnc::GUID guid;
    try {
        guid = gnc::GUID::from_string(best.account_guid);
    } catch (gnc::guid_syntax_exception&) {
        return nullptr;
    }
    auto account = xaccAccountLookup (reinterpret_cast<GncGUID*>(&guid), book);
    return account;
}

static void
change_imap_entry (Account *acc, std::string const & path, int64_t token_count)
{
    PINFO("Source Account is '%s', Count is '%" G_GINT64_FORMAT "'",
           xaccAccountGetName (acc), token_count);

    // check for existing guid entry
    if (auto existing_token_count = get_kvp_int64_path (acc, {path}))
    {
        PINFO("found existing value of '%" G_GINT64_FORMAT "'", *existing_token_count);
        token_count += *existing_token_count;
    }

    // Add or Update the entry based on guid
    set_kvp_int64_path (acc, {path}, token_count);
}

/** Updates the imap for a given account using a list of tokens */
void
gnc_account_imap_add_account_bayes (Account *acc,
                                    GList *tokens,
                                    Account *added_acc)
{
    GList *current_token;
    gint64 token_count;
    char *account_fullname;
    char *guid_string;

    ENTER(" ");
    if (!acc)
    {
        LEAVE(" ");
        return;
    }
    check_import_map_data (gnc_account_get_book(acc));

    g_return_if_fail (added_acc != nullptr);
    account_fullname = gnc_account_get_full_name(added_acc);
    xaccAccountBeginEdit (acc);

    PINFO("account name: '%s'", account_fullname);

    guid_string = guid_to_string (xaccAccountGetGUID (added_acc));

    /* process each token in the list */
    for (current_token = g_list_first(tokens); current_token;
            current_token = current_token->next)
    {
        char* token = static_cast<char*>(current_token->data);
        /* Jump to next iteration if the pointer is not valid or if the
                 string is empty. In HBCI import we almost always get an empty
                 string, which doesn't work in the kvp loopkup later. So we
                 skip this case here. */
        if (!token || !token[0])
            continue;
        /* start off with one token for this account */
        token_count = 1;
        PINFO("adding token '%s'", token);
        auto path = std::string {IMAP_FRAME_BAYES} + '/' + token + '/' + guid_string;
        /* change the imap entry for the account */
        change_imap_entry (acc, path, token_count);
    }
    /* free up the account fullname and guid string */
    xaccAccountCommitEdit (acc);
    gnc_features_set_used (gnc_account_get_book(acc), GNC_FEATURE_GUID_FLAT_BAYESIAN);
    g_free (account_fullname);
    g_free (guid_string);
    LEAVE(" ");
}

/*******************************************************************************/

static void
build_non_bayes (const char *key, const GValue *value, gpointer user_data)
{
    if (!G_VALUE_HOLDS_BOXED (value))
        return;
    QofBook     *book;
    GncGUID     *guid = nullptr;
    gchar       *guid_string = nullptr;
    auto imapInfo = (GncImapInfo*)user_data;
    // Get the book
    book = qof_instance_get_book (imapInfo->source_account);

    guid = (GncGUID*)g_value_get_boxed (value);
    guid_string = guid_to_string (guid);

    PINFO("build_non_bayes: match string '%s', match account guid: '%s'",
                            (char*)key, guid_string);

    auto imapInfo_node = static_cast <GncImapInfo*> (g_malloc(sizeof(GncImapInfo)));

    imapInfo_node->source_account = imapInfo->source_account;
    imapInfo_node->map_account    = xaccAccountLookup (guid, book);
    imapInfo_node->head           = g_strdup (imapInfo->head);
    imapInfo_node->match_string   = g_strdup (key);
    imapInfo_node->category       = g_strdup (imapInfo->category);
    imapInfo_node->count          = g_strdup (" ");

    imapInfo->list = g_list_prepend (imapInfo->list, imapInfo_node);

    g_free (guid_string);
}

static void
build_bayes (const char *suffix, KvpValue * value, GncImapInfo & imapInfo)
{
    size_t guid_start = strlen(suffix) - GUID_ENCODING_LENGTH;
    std::string account_guid {&suffix[guid_start]};
    GncGUID guid;
    try
    {
        guid = gnc::GUID::from_string (account_guid);
    }
    catch (const gnc::guid_syntax_exception& err)
    {
        PWARN("Invalid GUID string from %s%s", IMAP_FRAME_BAYES, suffix);
    }
    auto map_account = xaccAccountLookup (&guid, gnc_account_get_book (imapInfo.source_account));
    auto imap_node = static_cast <GncImapInfo*> (g_malloc (sizeof (GncImapInfo)));
    auto count = value->get <int64_t> ();
    imap_node->source_account = imapInfo.source_account;
    imap_node->map_account = map_account;
    imap_node->head = g_strdup_printf ("%s%s", IMAP_FRAME_BAYES, suffix);
    imap_node->match_string = g_strndup (&suffix[1], guid_start - 2);
    imap_node->category = g_strdup(" ");
    imap_node->count = g_strdup_printf ("%" G_GINT64_FORMAT, count);
    imapInfo.list = g_list_prepend (imapInfo.list, imap_node);
}

void gnc_account_imap_info_destroy (GncImapInfo* imapInfo)
{
    g_free (imapInfo->head);
    g_free (imapInfo->category);
    g_free (imapInfo->match_string);
    g_free (imapInfo->count);
    g_free (imapInfo);
}

GList *
gnc_account_imap_get_info_bayes (Account *acc)
{
    check_import_map_data (gnc_account_get_book (acc));
    /* A dummy object which is used to hold the specified account, and the list
     * of data about which we care. */
    GncImapInfo imapInfo {acc, nullptr};
    qof_instance_foreach_slot_prefix (QOF_INSTANCE (acc), IMAP_FRAME_BAYES, &build_bayes, imapInfo);
    return g_list_reverse(imapInfo.list);
}

GList *
gnc_account_imap_get_info (Account *acc, const char *category)
{
    GList *list = nullptr;

    GncImapInfo imapInfo;

    std::vector<std::string> path {IMAP_FRAME};
    if (category)
        path.emplace_back (category);

    imapInfo.source_account = acc;
    imapInfo.list = list;

    imapInfo.head = g_strdup (IMAP_FRAME);
    imapInfo.category = g_strdup (category);

    if (qof_instance_has_path_slot (QOF_INSTANCE (acc), path))
    {
        qof_instance_foreach_slot (QOF_INSTANCE(acc), IMAP_FRAME, category,
                                   build_non_bayes, &imapInfo);
    }
    g_free (imapInfo.head);
    g_free (imapInfo.category);
    return g_list_reverse(imapInfo.list);
}

/*******************************************************************************/

gchar *
gnc_account_get_map_entry (Account *acc, const char *head, const char *category)
{
    return g_strdup (category ?
                     get_kvp_string_path (acc, {head, category}) :
                     get_kvp_string_path (acc, {head}));
}


void
gnc_account_delete_map_entry (Account *acc, char *head, char *category,
                              char *match_string, gboolean empty)
{
    if (acc != nullptr)
    {
        std::vector<std::string> path {head};
        if (category)
            path.emplace_back (category);
        if (match_string)
            path.emplace_back (match_string);

        if (qof_instance_has_path_slot (QOF_INSTANCE (acc), path))
        {
            xaccAccountBeginEdit (acc);
            if (empty)
                qof_instance_slot_path_delete_if_empty (QOF_INSTANCE(acc), path);
            else
                qof_instance_slot_path_delete (QOF_INSTANCE(acc), path);
            PINFO("Account is '%s', head is '%s', category is '%s', match_string is'%s'",
                   xaccAccountGetName (acc), head, category, match_string);
            qof_instance_set_dirty (QOF_INSTANCE(acc));
            xaccAccountCommitEdit (acc);
        }
    }
}

void
gnc_account_delete_all_bayes_maps (Account *acc)
{
    if (acc != nullptr)
    {
        auto slots = qof_instance_get_slots_prefix (QOF_INSTANCE (acc), IMAP_FRAME_BAYES);
        if (!slots.size()) return;
        xaccAccountBeginEdit (acc);
        for (auto const & entry : slots)
        {
             qof_instance_slot_path_delete (QOF_INSTANCE (acc), {entry.first});
        }
        qof_instance_set_dirty (QOF_INSTANCE(acc));
        xaccAccountCommitEdit (acc);
    }
}

/* ================================================================ */
/* QofObject function implementation and registration */

static void
destroy_all_child_accounts (Account *acc, gpointer data)
{
    xaccAccountBeginEdit (acc);
    xaccAccountDestroy (acc);
}

static void
gnc_account_book_end(QofBook* book)
{
    Account *root_account = gnc_book_get_root_account (book);
    GList *accounts;

    if (!root_account)
        return;

    accounts = gnc_account_get_descendants (root_account);

    if (accounts)
    {
        accounts = g_list_reverse (accounts);
        g_list_foreach (accounts, (GFunc)destroy_all_child_accounts, nullptr);
        g_list_free (accounts);
    }
    xaccAccountBeginEdit (root_account);
    xaccAccountDestroy (root_account);
}

#ifdef _MSC_VER
/* MSVC compiler doesn't have C99 "designated initializers"
 * so we wrap them in a macro that is empty on MSVC. */
# define DI(x) /* */
#else
# define DI(x) x
#endif
static QofObject account_object_def =
{
    DI(.interface_version = ) QOF_OBJECT_VERSION,
    DI(.e_type            = ) GNC_ID_ACCOUNT,
    DI(.type_label        = ) "Account",
    DI(.create            = ) (void*(*)(QofBook*)) xaccMallocAccount,
    DI(.book_begin        = ) nullptr,
    DI(.book_end          = ) gnc_account_book_end,
    DI(.is_dirty          = ) qof_collection_is_dirty,
    DI(.mark_clean        = ) qof_collection_mark_clean,
    DI(.foreach           = ) qof_collection_foreach,
    DI(.printable         = ) (const char * (*)(gpointer)) xaccAccountGetName,
    DI(.version_cmp       = ) (int (*)(gpointer, gpointer)) qof_instance_version_cmp,
};

gboolean xaccAccountRegister (void)
{
    static QofParam params[] =
    {
        {
            ACCOUNT_NAME_, QOF_TYPE_STRING,
            (QofAccessFunc) xaccAccountGetName,
            (QofSetterFunc) xaccAccountSetName
        },
        {
            ACCOUNT_CODE_, QOF_TYPE_STRING,
            (QofAccessFunc) xaccAccountGetCode,
            (QofSetterFunc) xaccAccountSetCode
        },
        {
            ACCOUNT_DESCRIPTION_, QOF_TYPE_STRING,
            (QofAccessFunc) xaccAccountGetDescription,
            (QofSetterFunc) xaccAccountSetDescription
        },
        {
            ACCOUNT_COLOR_, QOF_TYPE_STRING,
            (QofAccessFunc) xaccAccountGetColor,
            (QofSetterFunc) xaccAccountSetColor
        },
        {
            ACCOUNT_FILTER_, QOF_TYPE_STRING,
            (QofAccessFunc) xaccAccountGetFilter,
            (QofSetterFunc) xaccAccountSetFilter
        },
        {
            ACCOUNT_SORT_ORDER_, QOF_TYPE_STRING,
            (QofAccessFunc) xaccAccountGetSortOrder,
            (QofSetterFunc) xaccAccountSetSortOrder
        },
        {
            ACCOUNT_SORT_REVERSED_, QOF_TYPE_BOOLEAN,
            (QofAccessFunc) xaccAccountGetSortReversed,
            (QofSetterFunc) xaccAccountSetSortReversed
        },
        {
            ACCOUNT_NOTES_, QOF_TYPE_STRING,
            (QofAccessFunc) xaccAccountGetNotes,
            (QofSetterFunc) xaccAccountSetNotes
        },
        {
            ACCOUNT_PRESENT_, QOF_TYPE_NUMERIC,
            (QofAccessFunc) xaccAccountGetPresentBalance, nullptr
        },
        {
            ACCOUNT_BALANCE_, QOF_TYPE_NUMERIC,
            (QofAccessFunc) xaccAccountGetBalance, nullptr
        },
        {
            ACCOUNT_CLEARED_, QOF_TYPE_NUMERIC,
            (QofAccessFunc) xaccAccountGetClearedBalance, nullptr
        },
        {
            ACCOUNT_RECONCILED_, QOF_TYPE_NUMERIC,
            (QofAccessFunc) xaccAccountGetReconciledBalance, nullptr
        },
        {
            ACCOUNT_TYPE_, QOF_TYPE_STRING,
            (QofAccessFunc) qofAccountGetTypeString,
            (QofSetterFunc) qofAccountSetType
        },
        {
            ACCOUNT_FUTURE_MINIMUM_, QOF_TYPE_NUMERIC,
            (QofAccessFunc) xaccAccountGetProjectedMinimumBalance, nullptr
        },
        {
            ACCOUNT_TAX_RELATED, QOF_TYPE_BOOLEAN,
            (QofAccessFunc) xaccAccountGetTaxRelated,
            (QofSetterFunc) xaccAccountSetTaxRelated
        },
        {
            ACCOUNT_OPENING_BALANCE_, QOF_TYPE_BOOLEAN,
            (QofAccessFunc) xaccAccountGetIsOpeningBalance,
            (QofSetterFunc) xaccAccountSetIsOpeningBalance
        },
        {
            ACCOUNT_SCU, QOF_TYPE_INT32,
            (QofAccessFunc) xaccAccountGetCommoditySCU,
            (QofSetterFunc) xaccAccountSetCommoditySCU
        },
        {
            ACCOUNT_NSCU, QOF_TYPE_BOOLEAN,
            (QofAccessFunc) xaccAccountGetNonStdSCU,
            (QofSetterFunc) xaccAccountSetNonStdSCU
        },
        {
            ACCOUNT_PARENT, GNC_ID_ACCOUNT,
            (QofAccessFunc) gnc_account_get_parent,
            (QofSetterFunc) qofAccountSetParent
        },
        {
            QOF_PARAM_BOOK, QOF_ID_BOOK,
            (QofAccessFunc) qof_instance_get_book, nullptr
        },
        {
            QOF_PARAM_GUID, QOF_TYPE_GUID,
            (QofAccessFunc) qof_instance_get_guid, nullptr
        },
        { nullptr },
    };

    qof_class_register (GNC_ID_ACCOUNT, (QofSortFunc) qof_xaccAccountOrder, params);

    return qof_object_register (&account_object_def);
}

/* ======================= UNIT TESTING ACCESS =======================
 * The following functions are for unit testing use only.
 */
static AccountPrivate*
utest_account_get_private (Account *acc)
{
    return GET_PRIVATE (acc);
}

AccountTestFunctions*
_utest_account_fill_functions(void)
{
    AccountTestFunctions* func = g_new(AccountTestFunctions, 1);

    func->get_private = utest_account_get_private;
    func->coll_get_root_account = gnc_coll_get_root_account;
    func->xaccFreeAccountChildren = xaccFreeAccountChildren;
    func->xaccFreeAccount = xaccFreeAccount;
    func->qofAccountSetParent = qofAccountSetParent;
    func->gnc_account_lookup_by_full_name_helper =
        gnc_account_lookup_by_full_name_helper;

    return func;
}
/* ======================= END OF FILE =========================== */
