/********************************************************************\
 * gnc-optiondb.cpp -- Collection of GncOption objects              *
 * Copyright (C) 2019 John Ralls <jralls@ceridwen.us>               *
 *                                                                  *
 * 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 <cstdint>
#include <functional>
#include <string>
#include <limits>
#include <sstream>
#include "gnc-option-uitype.hpp"
#include "kvp-value.hpp"
#include "kvp-frame.hpp"
#include "qofbookslots.h"
#include "guid.hpp"
#include "gnc-optiondb.h"
#include "gnc-optiondb.hpp"
#include "gnc-optiondb-impl.hpp"
#include "gnc-option-ui.hpp"

#include "gnc-session.h"
constexpr const char* log_module{G_LOG_DOMAIN};

constexpr auto stream_max = std::numeric_limits<std::streamsize>::max();
using AliasedOption = std::pair<const char*, const char*>;
using OptionAlias = std::pair<const char*, AliasedOption>;
using OptionAliases = std::vector<OptionAlias>;
class Aliases
{
    static const OptionAliases c_option_aliases;
public:
    static const AliasedOption* find_alias (const char* old_name)
    {
        if (!old_name) return nullptr;
        const auto alias =
            std::find_if(c_option_aliases.begin(), c_option_aliases.end(),
                         [old_name](auto alias){
                             return std::strcmp(old_name, alias.first) == 0;
                         });
        if (alias == c_option_aliases.end())
            return nullptr;

        return &alias->second;
    }
};

const OptionAliases Aliases::c_option_aliases
{
    {"Accounts to include", {nullptr, "Accounts"}},
    {"Exclude transactions between selected accounts?",
        {nullptr, "Exclude transactions between selected accounts"}},
    {"Filter Accounts", {nullptr, "Filter By…"}},
    {"Flatten list to depth limit?",
        {nullptr, "Flatten list to depth limit"}},
    {"From", {nullptr, "Start Date"}},
    {"Report Accounts", {nullptr, "Accounts"}},
    {"Report Currency", {nullptr, "Report's currency"}},
    {"Show Account Code?", {nullptr, "Show Account Code"}},
    {"Show Full Account Name?", {nullptr, "Show Full Account Name"}},
    {"Show Multi-currency Totals?",
        {nullptr, "Show Multi-currency Totals"}},
    {"Show zero balance items?", {nullptr, "Show zero balance items"}},
    {"Sign Reverses?", {nullptr, "Sign Reverses"}},
    {"To", {nullptr, "End Date"}},
    {"Charge Type", {nullptr, "Action"}}, // easy-invoice.scm, renamed June 2018
    // the following 4 options in income-gst-statement.scm renamed Dec 2018
    {"Individual income columns", {nullptr, "Individual sales columns"}},
    {"Individual expense columns",
        {nullptr, "Individual purchases columns"}},
    {"Remittance amount", {nullptr, "Gross Balance"}},
    {"Net Income", {nullptr, "Net Balance"}},
    // transaction.scm:
    {"Use Full Account Name?", {nullptr, "Use Full Account Name"}},
    {"Use Full Other Account Name?",
        {nullptr, "Use Full Other Account Name"}},
    {"Void Transactions?", {"Filter", "Void Transactions"}},
    {"Void Transactions", {"Filter", "Void Transactions"}},
    {"Account Substring", {"Filter", "Account Name Filter"}},
    {"Enable links", {nullptr, "Enable Links"}},
    // trep-engine: moved currency options to own tab
    {"Common Currency", {"Currency", "Common Currency"}},
    {"Show original currency amount",
        {"Currency", "Show original currency amount"}},
    {"Report's currency", {"Currency", "Report's currency"}},
    {"Reconcile Status", {nullptr, "Reconciled Status"}},
    // new-owner-report.scm, renamed Oct 2020 to differentiate with
    // Document Links:
    {"Links", {nullptr, "Transaction Links"}},
    // invoice.scm, renamed November 2018
    {"Individual Taxes", {nullptr, "Use Detailed Tax Summary"}},
    {"Show Accounts until level", {nullptr, "Levels of Subaccounts"}},
    {"Invoice number", {nullptr, "Invoice Number"}},
    {"Report title", {nullptr, "Report Title"}},
    {"Extra notes", {nullptr, "Extra Notes"}},
    // income-gst-statement.scm
    {"default format", {nullptr, "Default Format"}},
    {"Report format", {nullptr, "Report Format"}},
    // ... replaced to …, Dec 2022
    {"Filter By...", {nullptr, "Filter By…"}},
    {"Specify date to filter by...", {nullptr, "Specify date to filter by…"}},
    // trep-engine:
    {"Running Balance", {nullptr, "Account Balance"}},
    {"Totals", {nullptr, "Grand Total"}},
};

static bool
operator==(const std::string& str, const char* cstr)
{
    return strcmp(str.c_str(), cstr) == 0;
}

void
GncOptionSection::foreach_option(std::function<void(GncOption&)> func)
{
    std::for_each(m_options.begin(), m_options.end(), func);
}

void
GncOptionSection::foreach_option(std::function<void(const GncOption&)> func) const
{
    std::for_each(m_options.begin(), m_options.end(), func);
}

void
GncOptionSection::add_option(GncOption&& option)
{
    m_options.push_back(std::move(option));
    if (!std::is_sorted(m_options.begin(), m_options.end()))
        std::sort(m_options.begin(), m_options.end());
}

void
GncOptionSection::remove_option(const char* name)
{
    m_options.erase(std::remove_if(m_options.begin(), m_options.end(),
                                   [name](const auto& option) -> bool
                                   {
                                       return option.get_name() == name;
                                   }), m_options.end());
}

const GncOption*
GncOptionSection::find_option(const char* name) const
{
    auto option = std::find_if(m_options.begin(), m_options.end(),
                               [name](auto& option) -> bool {
                                   return option.get_name() == name;
                               });
    if (option != m_options.end())
        return &*option;

    auto alias = Aliases::find_alias(name);
    if (!alias || alias->first) // No alias or the alias
        return nullptr;         // is in a different section.
    return find_option(alias->second);
}

GncOptionDB::GncOptionDB() : m_default_section{} {}

GncOptionDB::GncOptionDB(QofBook* book) : GncOptionDB() {}

void
GncOptionDB::register_option(const char* sectname, GncOption&& option)
{
    auto section = find_section(sectname);

    if (section)
    {
        section->add_option(std::move(option));
        return;
    }

    m_sections.push_back(std::make_shared<GncOptionSection>(sectname));
    m_sections.back()->add_option(std::move(option));
    if (!std::is_sorted(m_sections.begin(), m_sections.end()))
        std::sort(m_sections.begin(), m_sections.end());
}

void
GncOptionDB::register_option(const char* sectname, GncOption* option)
{
    register_option(sectname, std::move(*option));
    delete option;
}

void
GncOptionDB::unregister_option(const char* sectname, const char* name)
{
    auto section = find_section(sectname);
    if (section)
        section->remove_option(name);
}

void
GncOptionDB::set_default_section(const char* sectname)
{
    m_default_section = find_section(sectname);
}

const GncOptionSection* const
GncOptionDB::get_default_section() const noexcept
{
        return m_default_section;
}

const GncOptionSection*
GncOptionDB::find_section(const std::string& section) const
{
    auto db_section = std::find_if(m_sections.begin(), m_sections.end(),
                                   [&section](auto& sect) -> bool
                                   {
                                       return section == sect->get_name();
                                   });
    return db_section == m_sections.end() ? nullptr : db_section->get();
}

const GncOption*
GncOptionDB::find_option(const std::string& section, const char* name) const
{
    auto db_section = const_cast<GncOptionDB*>(this)->find_section(section);
    const GncOption* option = nullptr;
    if (db_section)
        option = db_section->find_option(name);
    if (option)
        return option;
    auto alias = Aliases::find_alias(name);
     /* Only try again if alias.first isn't
     * nullptr. GncOptionSection::find_option already checked if the alias
     * should have been in the same section.
     */
    if (alias && alias->first && section != alias->first)
        return find_option(alias->first, alias->second);
    return nullptr;
}

std::string
GncOptionDB::lookup_string_option(const char* section, const char* name)
{
    static const std::string empty_string{};

    auto db_opt = find_option(section, name);
    if (!db_opt)
        return empty_string;
    return db_opt->get_value<std::string>();
}

void
GncOptionDB::make_internal(const char* section, const char* name)
{

    auto db_opt = find_option(section, name);
    if (db_opt)
        db_opt->make_internal();
}

std::ostream&
GncOptionDB::save_option_key_value(std::ostream& oss,
                                   const std::string& section,
                                   const std::string& name) const noexcept
{

    auto db_opt = find_option(section, name.c_str());
    if (!db_opt || !db_opt->is_changed())
        return oss;
    oss << section.substr(0, classifier_size_max) << ":" <<
        name.substr(0, classifier_size_max) << "=" << *db_opt << ";";
    return oss;
}

std::istream&
GncOptionDB::load_option_key_value(std::istream& iss)
{

    char section[classifier_size_max], name[classifier_size_max];
    iss.getline(section, classifier_size_max, ':');
    iss.getline(name, classifier_size_max, '=');
    if (!iss)
        throw std::invalid_argument("Section or name delimiter not found or values too long");
    auto option = find_option(section, name);
    if (!option)
        iss.ignore(stream_max, ';');
    else
    {
        std::string value;
        std::getline(iss, value, ';');
        std::istringstream item_iss{value};
        item_iss >> *option;
    }
    return iss;
}

std::ostream&
GncOptionDB::save_to_key_value(std::ostream& oss) const noexcept
{

    foreach_section(
        [&oss](const GncOptionSectionPtr& section)
        {
            oss << "[Options]\n";
            section->foreach_option(
                [&oss, &section](auto& option)
                {
                    if (option.is_changed())
                        oss << section->get_name().substr(0, classifier_size_max) <<
                            ':' << option.get_name().substr(0, classifier_size_max) <<
                            '=' << option << '\n';
                });
        });
    return oss;
}

std::istream&
GncOptionDB::load_from_key_value(std::istream& iss)
{
    if (iss.peek() == '[')
    {
        char buf[classifier_size_max];
        iss.getline(buf, classifier_size_max);
        if (strcmp(buf, "[Options]") != 0) // safe
            throw std::runtime_error("Wrong secion header for options.");
    }
    // Otherwise assume we were sent here correctly:
    while (iss.peek() != '[') //Indicates the start of the next file section
    {
        load_option_key_value(iss);
    }
    return iss;
}

size_t
GncOptionDB::register_callback(GncOptionDBChangeCallback cb, void* data)
{
    constexpr std::hash<GncOptionDBChangeCallback> cb_hash;
    auto id{cb_hash(cb)};
    if (std::find_if(m_callbacks.begin(), m_callbacks.end(),
                     [id](auto&cb)->bool{ return cb.m_id == id; }) == m_callbacks.end())
        m_callbacks.emplace_back(id, cb, data);
    return id;
}

void
GncOptionDB::unregister_callback(size_t id)
{
    m_callbacks.erase(std::remove_if(m_callbacks.begin(), m_callbacks.end(),
                                     [id](auto& cb)->bool { return cb.m_id == id; }),
                      m_callbacks.end());
}

void
GncOptionDB::run_callbacks()
{
    std::for_each(m_callbacks.begin(), m_callbacks.end(),
                  [](auto& cb)->void { cb.m_func(cb.m_data); });
}

static inline void
counter_option_path(const GncOption& option, GSList* list, std::string& name)
{
    constexpr const char* counters{"counters"};
    constexpr const char* formats{"counter_formats"};
    auto key = option.get_key();
    name = key.substr(0, key.size() - 1);
    list->next->data = (void*)name.c_str();
    if (option.get_name().rfind("format")
        != std::string::npos)
        list->data = (void*)formats;
    else
        list->data = (void*)counters;
}

static inline void
option_path(const GncOption& option, GSList* list)
{
    list->next->data = (void*)option.get_name().c_str();
    list->data = (void*)option.get_section().c_str();
}

/* The usage "option.template get_value<bool>()" looks weird, but it's required
 * by the C++ standard: "When the name of a member template specialization
 * appears after . or -> in a postfix-expression, or after nested-name-specifier
 * in a qualified-id, and the postfix-expression or qualified-id explicitly
 * depends on a template-parameter (14.6.2), the member template name must be
 * prefixed by the keyword template. Otherwise the name is assumed to name a
 * non-template."
 */
static inline KvpValue*
kvp_value_from_bool_option(const GncOption& option)
{
    auto val{option.template get_value<bool>()};
    // ~KvpValue will g_free the value.
    return new KvpValue(val ? g_strdup("t") : g_strdup("f"));
}

static bool
is_qofinstance_ui_type(GncOptionUIType type)
{
    switch (type)
    {
        case GncOptionUIType::ACCOUNT_SEL:
        case GncOptionUIType::BUDGET:
        case GncOptionUIType::OWNER:
        case GncOptionUIType::CUSTOMER:
        case GncOptionUIType::VENDOR:
        case GncOptionUIType::EMPLOYEE:
        case GncOptionUIType::INVOICE:
        case GncOptionUIType::TAX_TABLE:
        case GncOptionUIType::QUERY:
            return true;
        default:
            return false;
    }
}

static inline KvpValue*
kvp_value_from_qof_instance_option(const GncOption& option)
{
    const QofInstance* inst{QOF_INSTANCE(option.template get_value<const QofInstance*>())};
    auto guid = guid_copy(qof_instance_get_guid(inst));
    return new KvpValue(guid);
}

/* GncOptionDateFormat Constants and support functions. These are frozen for backwards compatibility. */

static const char* date_format_frame_key = "Fancy Date Format";
static const char* date_format_custom_key ="custom";
static const char* date_format_months_key = "month";
static const char* date_format_years_key = "years";
static const char *date_format_format_key = "fmt";

static inline KvpValue *
kvp_frame_from_date_format_option(const GncOption& option)
{
    auto [format, months, years, custom] = option.get_value<GncOptionDateFormat>();

    if (format == QOF_DATE_FORMAT_UNSET)
        return nullptr;

    auto frame{new KvpFrame};
    frame->set({date_format_format_key}, new KvpValue(g_strdup(gnc_date_dateformat_to_string(format))));
    frame->set({date_format_months_key}, new KvpValue(g_strdup(gnc_date_monthformat_to_string(months))));
    frame->set({date_format_years_key}, new KvpValue(static_cast<int64_t>(years)));
    frame->set({date_format_custom_key}, new KvpValue(g_strdup(custom.c_str())));
    return new KvpValue(frame);
};

void
GncOptionDB::save_to_kvp(QofBook* book, bool clear_options) const noexcept
{
    if (clear_options)
        qof_book_options_delete(book, nullptr);
    const_cast<GncOptionDB*>(this)->foreach_section(
        [book](GncOptionSectionPtr& section)
        {
            section->foreach_option(
                [book, &section](GncOption& option) {
                    if (option.is_dirty())
                    {
                        /* We need the string name out here so that it stays in
                         * scope long enough to pass its c_str to
                         * gnc_book_set_option. */
                        std::string name;
                        /* qof_book_set_option wants a GSList path. Let's avoid
                         * allocating and make one here. */
                        GSList list_tail{}, list_head{nullptr, &list_tail};
                        if (strcmp(section->get_name().c_str(), "Counters") == 0)
                            counter_option_path(option, &list_head, name);
                        else
                            option_path(option, &list_head);
                        auto type{option.get_ui_type()};
                        KvpValue* kvp{};
                        if (type == GncOptionUIType::BOOLEAN)
                            kvp = kvp_value_from_bool_option(option);
                        else if (is_qofinstance_ui_type(type))
                            kvp = kvp_value_from_qof_instance_option(option);
                        else if (type == GncOptionUIType::NUMBER_RANGE)
                        {
                            if (option.is_alternate())
                            {
                                kvp = new KvpValue(static_cast<int64_t>(option.template get_value<int>()));
                            }
                            else
                            {
                                kvp = new KvpValue(option.template get_value<double>());
                            }
                        }
                        else if (type == GncOptionUIType::DATE_FORMAT)
                            kvp = kvp_frame_from_date_format_option(option);
                        else
                        {
                            auto str{option.template get_value<std::string>()};
                            kvp = new KvpValue{g_strdup(str.c_str())};
                        }
                        qof_book_set_option(book, kvp, &list_head);
                        option.mark_saved();
                    }
                });
        });
}

static inline void
fill_option_from_string_kvp(GncOption& option, KvpValue* kvp)
{
    auto str{kvp->get<const char*>()};
    if (option.get_ui_type() == GncOptionUIType::BOOLEAN)
        option.set_value(*str == 't' ? true : false);
    else
        option.set_value(std::string{str});
}

static inline void
fill_option_from_guid_kvp(GncOption& option, KvpValue* kvp)
{
    auto guid{kvp->get<GncGUID*>()};
    option.set_value(
        (const QofInstance*)qof_instance_from_guid(guid, option.get_ui_type()));
}

static inline void
fill_option_from_date_format_kvp(GncOption& option, KvpValue* kvp)
{
    GncOptionDateFormat default_fmt{QOF_DATE_FORMAT_UNSET, GNCDATE_MONTH_NUMBER, true, ""};
    auto frame{kvp->get<KvpFrame*>()};
    if (!frame)
    {
        option.set_value(default_fmt);
        return;
    }
    auto format_str{frame->get_slot({date_format_format_key})->get<const char*>()};
    QofDateFormat format;
    if (!format_str || gnc_date_string_to_dateformat(format_str, &format))
    {
        option.set_value(default_fmt);
        return;
    }
    GNCDateMonthFormat months = GNCDATE_MONTH_NUMBER;
    auto months_str{frame->get_slot({date_format_months_key})->get<const char*>()};
    if (months_str)
        gnc_date_string_to_monthformat(months_str, &months);
    auto years_num{frame->get_slot({date_format_years_key})->get<int64_t>()};
    bool years = static_cast<bool>(years_num);
    auto custom_str{frame->get_slot({date_format_custom_key})->get<const char*>()};
    option.set_value<GncOptionDateFormat>({format, months, years, custom_str ? custom_str : ""});
}

void
GncOptionDB::load_from_kvp(QofBook* book) noexcept
{
    foreach_section(
        [book](GncOptionSectionPtr& section)
        {
            section->foreach_option(
                [book, &section](GncOption& option)
                {
                    // Make path list as above.
                    std::string name;
                    /* qof_book_set_option wants a GSList path. Let's avoid
                     * allocating and make one here. */
                    GSList list_tail{}, list_head{nullptr, &list_tail};
                    if (strcmp(section->get_name().c_str(), "Counters") == 0)
                        counter_option_path(option, &list_head, name);
                    else
                        option_path(option, &list_head);
                    auto kvp = qof_book_get_option(book, &list_head);
                    if (!kvp)
                        return;

                    auto set_double = [&option, kvp, &list_head]() {
                        /*counters might have been set as doubles
                         * because of
                         * https://bugs.gnucash.org/show_bug.cgi?id=798930. They
                         * should be int.
                         */
                            constexpr const char *counters{"counters"};
                            auto value{kvp->get<double>()};
                            if (strcmp(static_cast<char*>(list_head.data), counters) == 0)
                                option.set_value(static_cast<int>(value));
                            else
                                option.set_value(value);
                    };

                    switch (kvp->get_type())
                    {
                        case KvpValue::Type::DOUBLE:
                            set_double();
                            break;
                        case KvpValue::Type::INT64:
                            option.set_value(static_cast<int>(kvp->get<int64_t>()));
                            break;
                        case KvpValue::Type::STRING:
                            fill_option_from_string_kvp(option, kvp);
                            break;
                        case KvpValue::Type::GUID:
                            fill_option_from_guid_kvp(option, kvp);
                          break;
                        case KvpValue::Type::FRAME:
                            if (g_strcmp0(option.get_name().c_str(), date_format_frame_key) == 0)
                                fill_option_from_date_format_kvp(option, kvp);
                            break;
                        default:
                            return;
                            break;
                    }
                });
        });
}

void
gnc_register_string_option(GncOptionDB* db, const char* section,
                           const char* name, const char* key,
                           const char* doc_string, std::string value)
{
    GncOption option{section, name, key, doc_string, value,
            GncOptionUIType::STRING};
    db->register_option(section, std::move(option));
}

void
gnc_register_text_option(GncOptionDB* db, const char* section, const char* name,
                         const char* key, const char* doc_string,
                         std::string value)
{
    GncOption option{section, name, key, doc_string, value,
            GncOptionUIType::TEXT};
    db->register_option(section, std::move(option));

}

void
gnc_register_font_option(GncOptionDB* db, const char* section,
                         const char* name, const char* key,
                         const char* doc_string, std::string value)
{
    GncOption option{section, name, key, doc_string, value,
            GncOptionUIType::FONT};
    db->register_option(section, std::move(option));
}

void
gnc_register_budget_option(GncOptionDB* db, const char* section,
                           const char* name, const char* key,
                           const char* doc_string, GncBudget *value)
{
    GncOption option{GncOptionQofInstanceValue{section, name, key, doc_string,
                                               (const QofInstance*)value,
                                               GncOptionUIType::BUDGET}};
    db->register_option(section, std::move(option));
}

void
gnc_register_color_option(GncOptionDB* db, const char* section,
                         const char* name, const char* key,
                         const char* doc_string, std::string value)
{
    GncOption option{section, name, key, doc_string, value,
            GncOptionUIType::COLOR};
    db->register_option(section, std::move(option));
}

void
gnc_register_commodity_option(GncOptionDB* db, const char* section,
                              const char* name, const char* key,
                              const char* doc_string, gnc_commodity *value)
{
    GncOption option{GncOptionCommodityValue{section, name, key, doc_string,
                                               value,
                                               GncOptionUIType::COMMODITY}};
    db->register_option(section, std::move(option));
}

void
gnc_register_commodity_option(GncOptionDB* db, const char* section,
                              const char* name, const char* key,
                              const char* doc_string, const char* value)
{
    gnc_commodity* commodity{};
    const auto book{qof_session_get_book(gnc_get_current_session())};
    const auto commodity_table{gnc_commodity_table_get_table(book)};
    const auto namespaces{gnc_commodity_table_get_namespaces(commodity_table)};
    for (auto node = namespaces; node && commodity == nullptr;
         node = g_list_next(node))
    {
        commodity = gnc_commodity_table_lookup(commodity_table,
                                               (const char*)(node->data),
                                               value);
        if (commodity)
            break;
    }
    GncOption option{GncOptionCommodityValue{section, name, key, doc_string,
                commodity,
                GncOptionUIType::COMMODITY}};
    db->register_option(section, std::move(option));
    g_list_free (namespaces);
}

void
gnc_register_simple_boolean_option(GncOptionDB* db,
                                   const char* section, const char* name,
                                   const char* key, const char* doc_string,
                                   bool value)
{
    GncOption option{section, name, key, doc_string, value,
            GncOptionUIType::BOOLEAN};
    db->register_option(section, std::move(option));
}

void
gnc_register_pixmap_option(GncOptionDB* db, const char* section,
                           const char* name, const char* key,
                           const char* doc_string, std::string value)
{
    GncOption option{section, name, key, doc_string, value,
            GncOptionUIType::PIXMAP};
    db->register_option(section, std::move(option));
}

void
gnc_register_account_list_option(GncOptionDB* db, const char* section,
                                 const char* name, const char* key,
                                 const char* doc_string,
                                 const GncOptionAccountList& value)
{
    GncOption option{GncOptionAccountListValue{section, name, key, doc_string,
                GncOptionUIType::ACCOUNT_LIST, value}};
    db->register_option(section, std::move(option));
}

void
gnc_register_account_list_limited_option(GncOptionDB* db,
                                         const char* section, const char* name,
                                         const char* key,
                                         const char* doc_string,
                                         const GncOptionAccountList& value,
                                         GncOptionAccountTypeList&& allowed)
{
    try
    {
        GncOption option{GncOptionAccountListValue{section, name, key, doc_string,
                    GncOptionUIType::ACCOUNT_LIST, value, std::move(allowed)}};
        db->register_option(section, std::move(option));
    }
    catch (const std::invalid_argument& err)
    {
        PWARN("Account List Limited Option, value failed validation, option not registered.");
    }
}

using AccountPair = std::pair<GncOptionAccountList&,
                              const GncOptionAccountTypeList&>;
static void
find_children(Account* account, void* data)
{
    auto datapair =
        (AccountPair*)data;
    GncOptionAccountList& list = datapair->first;
    const GncOptionAccountTypeList& types = datapair->second;
    if (std::find(types.begin(), types.end(),
                  xaccAccountGetType(account)) != types.end())
        list.push_back(*qof_entity_get_guid(account));
}

GncOptionAccountList
gnc_account_list_from_types(QofBook *book,
                            const GncOptionAccountTypeList& types)
{
    GncOptionAccountList list;
    AccountPair funcdata{list, types};
    Account* base_acct = gnc_book_get_root_account(book);
    gnc_account_foreach_descendant(base_acct, (AccountCb)find_children,
                                   &funcdata);
    return list;
}


void
gnc_register_account_sel_limited_option(GncOptionDB* db,
                                        const char* section, const char* name,
                                        const char* key, const char* doc_string,
                                        const Account* value,
                                        GncOptionAccountTypeList&& allowed)
{
    try
    {
        GncOption option{GncOptionAccountSelValue{section, name, key, doc_string,
                    GncOptionUIType::ACCOUNT_SEL, value, std::move(allowed)}};
    db->register_option(section, std::move(option));
    }
    catch (const std::invalid_argument& err)
    {
        PWARN("Account Sel Limited Option, value failed validation, option not registerd.");
    }
}

void
gnc_register_multichoice_option(GncOptionDB* db, const char* section,
                                const char* name, const char* key,
                                const char* doc_string, const char* default_val,
                                GncMultichoiceOptionChoices&& choices)
{
    std::string defval{default_val};
    auto found{std::find_if(choices.begin(), choices.end(),
                            [&defval](auto& choice)->bool {
                                return defval == std::get<0>(choice);
                            })};
    if (found == choices.end())
        defval = (choices.empty() ? std::string{"None"} :
                  std::get<0>(choices.at(0)));
    GncOption option{GncOptionMultichoiceValue{section, name, key, doc_string,
                defval.c_str(), std::move(choices)}};
    db->register_option(section, std::move(option));
}

void
gnc_register_list_option(GncOptionDB* db, const char* section,
                         const char* name, const char* key,
                         const char* doc_string, const char* value,
                         GncMultichoiceOptionChoices&& list)
{
    GncOption option{GncOptionMultichoiceValue{section, name, key, doc_string,
                value,  std::move(list), GncOptionUIType::LIST}};
    db->register_option(section, std::move(option));
}

/* Only balance-forecast.scm, sample-report.scm, and net-charts.scm
 * use decimals and fractional steps and they can be worked around.
 */
template <typename ValueType> void
gnc_register_number_range_option(GncOptionDB* db, const char* section,
                                 const char* name, const char* key,
                                 const char* doc_string, ValueType value,
                                 ValueType min, ValueType max, ValueType step)
{
    try
    {
        GncOption option{GncOptionRangeValue<ValueType>{section, name, key,
                                                        doc_string, value, min,
                                                        max, step}};
        db->register_option(section, std::move(option));
    }
    catch(const std::invalid_argument& err)
    {
        PWARN("Number Range Option %s, option not registerd.",
              err.what());
    }
}

void
gnc_register_number_plot_size_option(GncOptionDB* db,
                                     const char* section, const char* name,
                                     const char* key, const char* doc_string,
                                     int value)
{
//65K is 10x reasonable, but it's a convenient constant.
    GncOption option{GncOptionRangeValue<int>{section, name, key, doc_string,
            value, 10, UINT16_MAX, 1, GncOptionUIType::PLOT_SIZE}};
    db->register_option(section, std::move(option));
}

void
gnc_register_query_option(GncOptionDB* db, const char* section,
                          const char* name, const QofQuery* value)
{
    GncOption option{section, name, "", "", value,
            GncOptionUIType::INTERNAL};
    db->register_option(section, std::move(option));
}

void
gnc_register_owner_option(GncOptionDB* db, const char* section,
                          const char* name, const char* key,
                          const char* doc_string, const GncOwner* value,
                          GncOwnerType type)
{
    GncOptionUIType uitype;
    switch (type)
    {
    case GNC_OWNER_CUSTOMER:
        uitype = GncOptionUIType::CUSTOMER;
        break;
    case GNC_OWNER_EMPLOYEE:
        uitype = GncOptionUIType::EMPLOYEE;
        break;
    case GNC_OWNER_JOB:
        uitype = GncOptionUIType::JOB;
        break;
    case GNC_OWNER_VENDOR:
        uitype = GncOptionUIType::VENDOR;
        break;
    default:
        uitype = GncOptionUIType::INTERNAL;
    };
    GncOption option{GncOptionGncOwnerValue{section, name, key, doc_string,
                                            value, uitype}};
    db->register_option(section, std::move(option));
}

void
gnc_register_invoice_option(GncOptionDB* db, const char* section,
                            const char* name, const char* key,
                            const char* doc_string, GncInvoice* value)
{
    GncOption option{GncOptionQofInstanceValue{section, name, key, doc_string,
                                               (const QofInstance*)value,
                                               GncOptionUIType::INVOICE}};
    db->register_option(section, std::move(option));
}

void
gnc_register_taxtable_option(GncOptionDB* db, const char* section,
                             const char* name, const char* key,
                             const char* doc_string, GncTaxTable* value)
{
    GncOption option{GncOptionQofInstanceValue{section, name, key, doc_string,
                                               (const QofInstance*)value,
                                               GncOptionUIType::TAX_TABLE}};
    db->register_option(section, std::move(option));
}

void
gnc_register_invoice_print_report_option(GncOptionDB* db, const char* section,
                                         const char* name, const char* key,
                                         const char* doc_string, std::string value)
{
    GncOption option{section, name, key, doc_string,
                     value, GncOptionUIType::INV_REPORT};
    db->register_option(section, std::move(option));
}

void
gnc_register_counter_option(GncOptionDB* db, const char* section,
                            const char* name, const char* key,
                            const char* doc_string, int value)
{
    GncOption option{GncOptionRangeValue<int>{section, name, key, doc_string,
                value, 0, 999999999, 1}};
    option.set_alternate(true);
    db->register_option(section, std::move(option));
}

void
gnc_register_counter_format_option(GncOptionDB* db,
                                   const char* section, const char* name,
                                   const char* key, const char* doc_string,
                                   std::string value)
{
    GncOption option{section, name, key, doc_string, value,
            GncOptionUIType::STRING};
    db->register_option(section, std::move(option));
}

void
gnc_register_dateformat_option(GncOptionDB* db, const char* section,
                               const char* name, const char* key,
                               const char* doc_string, GncOptionDateFormat&& value)
{
    GncOption option{section, name, key, doc_string, std::move(value),
            GncOptionUIType::DATE_FORMAT};
    db->register_option(section, std::move(option));
}

void
gnc_register_currency_option(GncOptionDB* db, const char* section,
                             const char* name, const char* key,
                             const char* doc_string, gnc_commodity *value)
{
    GncOption option{GncOptionCommodityValue{
        section, name, key, doc_string, value, GncOptionUIType::CURRENCY
        }};
    db->register_option(section, std::move(option));
}

void
gnc_register_currency_option(GncOptionDB* db, const char* section,
                             const char* name, const char* key,
                             const char* doc_string, const char* value)
{
    const auto book{qof_session_get_book(gnc_get_current_session())};
    const auto commodity_table{gnc_commodity_table_get_table(book)};
    const auto commodity = gnc_commodity_table_lookup(commodity_table,
                                                      "CURRENCY",
                                                      value);
    GncOption option{GncOptionCommodityValue{
        section, name, key, doc_string, commodity, GncOptionUIType::CURRENCY
        }};
    db->register_option(section, std::move(option));
}

void
gnc_register_date_option(GncOptionDB* db, const char* section,
                         const char* name, const char* key,
                         const char* doc_string, time64 time,
                         RelativeDateUI ui)
{
    auto ui_type = ui == RelativeDateUI::BOTH ? GncOptionUIType::DATE_BOTH :
        ui == RelativeDateUI::RELATIVE ? GncOptionUIType::DATE_RELATIVE :
        GncOptionUIType::DATE_ABSOLUTE;
    GncOption option{GncOptionDateValue(section, name, key, doc_string,
                                        ui_type, time)};
    db->register_option(section, std::move(option));
}

void
gnc_register_date_option(GncOptionDB* db, const char* section,
                         const char* name, const char* key,
                         const char* doc_string, RelativeDatePeriod period,
                         RelativeDateUI ui)
{
    auto ui_type = ui == RelativeDateUI::BOTH ? GncOptionUIType::DATE_BOTH :
        ui == RelativeDateUI::RELATIVE ? GncOptionUIType::DATE_RELATIVE :
        GncOptionUIType::DATE_ABSOLUTE;
    GncOption option{GncOptionDateValue(section, name, key, doc_string,
                                        ui_type, period)};
    db->register_option(section, std::move(option));
}

void
gnc_register_date_option(GncOptionDB* db,
                                  const char* section, const char* name,
                                  const char* key, const char* doc_string,
                                  RelativeDatePeriodVec& period_set,
                                  bool both)
{
    auto is_absolute = period_set.size() == 1 &&
                       period_set.front() == RelativeDatePeriod::ABSOLUTE;
    auto ui_type = both ? GncOptionUIType::DATE_BOTH :
        is_absolute ? GncOptionUIType::DATE_ABSOLUTE : GncOptionUIType::DATE_RELATIVE;
    GncOption option{GncOptionDateValue(section, name, key, doc_string,
                                        ui_type, period_set)};
    if (is_absolute)
        option.set_default_value(gnc_time(nullptr));
    db->register_option(section, std::move(option));
}


static const RelativeDatePeriodVec begin_dates
{
    RelativeDatePeriod::TODAY,
    RelativeDatePeriod::START_THIS_MONTH,
    RelativeDatePeriod::START_PREV_MONTH,
    RelativeDatePeriod::START_CURRENT_QUARTER,
    RelativeDatePeriod::START_PREV_QUARTER,
    RelativeDatePeriod::START_CAL_YEAR,
    RelativeDatePeriod::START_PREV_YEAR,
    RelativeDatePeriod::START_ACCOUNTING_PERIOD
};

void
gnc_register_start_date_option(GncOptionDB* db, const char* section,
                               const char* name, const char* key,
                               const char* doc_string, bool both)
{
    auto ui_type = both ? GncOptionUIType::DATE_BOTH :
        GncOptionUIType::DATE_RELATIVE;
    GncOption option{GncOptionDateValue(section, name, key, doc_string,
                                        ui_type, begin_dates)};
    db->register_option(section, std::move(option));
}

static const RelativeDatePeriodVec end_dates
{
    RelativeDatePeriod::TODAY,
    RelativeDatePeriod::END_THIS_MONTH,
    RelativeDatePeriod::END_PREV_MONTH,
    RelativeDatePeriod::END_CURRENT_QUARTER,
    RelativeDatePeriod::END_PREV_QUARTER,
    RelativeDatePeriod::END_CAL_YEAR,
    RelativeDatePeriod::END_PREV_YEAR,
    RelativeDatePeriod::END_ACCOUNTING_PERIOD
};

void
gnc_register_end_date_option(GncOptionDB* db, const char* section,
                             const char* name, const char* key,
                             const char* doc_string, bool both)
{
    auto ui_type = both ? GncOptionUIType::DATE_BOTH :
        GncOptionUIType::DATE_RELATIVE;
    GncOption option{GncOptionDateValue(section, name, key, doc_string,
                                        ui_type, end_dates)};
    db->register_option(section, std::move(option));
}

void
gnc_register_report_placement_option(GncOptionDBPtr& db,
                                     const char* section, const char* name)
{
    /* This is a special option with it's own UI file so we have fake values to pass
     * to the template creation function.
     */
    GncOptionReportPlacementVec value;
    GncOption option{GncOptionValue<GncOptionReportPlacementVec>{section, name,
                                                              "no_key", "nodoc_string",
                                                              value,GncOptionUIType::REPORT_PLACEMENT}};
    db->register_option(section, std::move(option));
}

void
gnc_register_internal_option(GncOptionDBPtr& db,
                             const char* section, const char* name,
                             const std::string& value)
{
    GncOption option{
        GncOptionValue<std::string>{section, name, "", "", value,
                                    GncOptionUIType::INTERNAL}};
    db->register_option(section, std::move(option));
}

void
gnc_register_internal_option(GncOptionDBPtr& db,
                             const char* section, const char* name,
                             bool value)
{
    GncOption option{
        GncOptionValue<bool>{section, name, "", "", value,
                             GncOptionUIType::INTERNAL}};
    db->register_option(section, std::move(option));
}

GncOptionDB*
gnc_option_db_new(void)
{
    return new GncOptionDB;
}

void
gnc_option_db_destroy(GncOptionDB* odb)
{
    PWARN("Direct Destroy called on GncOptionDB %" G_GUINT64_FORMAT, (uint64_t)odb);
}

GList*
gnc_option_db_commit(GncOptionDB* odb)
{
    GList* errors{};
    odb->foreach_section(
        [&errors](GncOptionSectionPtr& section){
            section->foreach_option(
                [&errors](GncOption& option) {
                    try
                    {
                        option.set_option_from_ui_item();
                    }
                    catch (const std::invalid_argument& err)
                    {
                        PWARN("Option %s:%s failed to set its value %s",
                              option.get_section().c_str(),
                              option.get_name().c_str(), err.what());
                        errors = g_list_prepend(errors,
                                                (void*)option.get_name().c_str());
                    } });
        });
    if (!errors)
        odb->run_callbacks();
    return errors;
}

void
gnc_option_db_clean(GncOptionDB* odb)
{
        odb->foreach_section(
        [](GncOptionSectionPtr& section){
            section->foreach_option(
                [](GncOption& option) {
                    option.set_ui_item_from_option();
                });
        });
}

void gnc_option_db_load(GncOptionDB* odb, QofBook* book)
{
    odb->load_from_kvp(book);
}

void
gnc_option_db_save(GncOptionDB* odb, QofBook* book,
                        gboolean clear_options)
{
    odb->save_to_kvp(book, static_cast<bool>(clear_options));
}

void
gnc_option_db_book_options(GncOptionDB* odb)
{
    constexpr const char* business_section{N_("Business")};
    constexpr const char* counter_section{N_("Counters")};
    static const std::string empty_string{""};

//Accounts Tab

    gnc_register_number_range_option<double>(odb, OPTION_SECTION_ACCOUNTS,
                                     OPTION_NAME_AUTO_READONLY_DAYS, "a",
                                     N_("Choose the number of days after which transactions will be read-only and cannot be edited anymore. This threshold is marked by a red line in the account register windows. If zero, all transactions can be edited and none are read-only."),
                                     0.0, 0.0, 3650.0, 1.0);

    gnc_register_simple_boolean_option(odb, OPTION_SECTION_ACCOUNTS,
                                       OPTION_NAME_NUM_FIELD_SOURCE, "b",
                                       N_("Check to have split action field used in registers for 'Num' field in place of transaction number; transaction number shown as 'T-Num' on second line of register. Has corresponding effect on business features, reporting and imports/exports."),
                                       false);
    gnc_register_simple_boolean_option(odb, OPTION_SECTION_ACCOUNTS,
                                       OPTION_NAME_TRADING_ACCOUNTS, "a",
                                       N_("Check to have trading accounts used for transactions involving more than one currency or commodity."),
                                       false);

//Budgeting Tab

    gnc_register_budget_option(odb, OPTION_SECTION_BUDGETING,
                               OPTION_NAME_DEFAULT_BUDGET, "a",
                               N_("Budget to be used when none has been otherwise specified."),
                               nullptr);

//Counters Tab

    gnc_register_counter_option(odb, counter_section,
                                N_("Customer number"), "gncCustomera",
                                N_("The previous customer number generated. This number will be incremented to generate the next customer number."),
                                0);
    gnc_register_counter_format_option(odb, counter_section,
                                       N_("Customer number format"),
                                       "gncCustomerb",
                                       N_("The format string to use for generating customer numbers. This is a printf-style format string."),
                                       empty_string);
    gnc_register_counter_option(odb, counter_section,
                                N_("Employee number"), "gncEmployeea",
                                N_("The previous employee number generated. This number will be incremented to generate the next employee number."),
                                0);
    gnc_register_counter_format_option(odb, counter_section,
                                       N_("Employee number format"),
                                       "gncEmployeeb",
                                       N_("The format string to use for generating employee numbers. This is a printf-style format string."),
                                       empty_string);
    gnc_register_counter_option(odb, counter_section,
                                N_("Invoice number"), "gncInvoicea",
                                N_("The previous invoice number generated. This number will be incremented to generate the next invoice number."),
                                0);
    gnc_register_counter_format_option(odb, counter_section,
                                       N_("Invoice number format"),
                                       "gncInvoiceb",
                                       N_("The format string to use for generating invoice numbers. This is a printf-style format string."),
                                       empty_string);
    gnc_register_counter_option(odb, counter_section,
                                N_("Bill number"), "gncBilla",
                                N_("The previous bill number generated. This number will be incremented to generate the next bill number."),
                                0);
    gnc_register_counter_format_option(odb, counter_section,
                                       N_("Bill number format"), "gncBillb",
                                       N_("The format string to use for generating bill numbers. This is a printf-style format string."),
                                       empty_string);
    gnc_register_counter_option(odb, counter_section,
                                N_("Expense voucher number"), "gncExpVouchera",
                                N_("The previous expense voucher number generated. This number will be incremented to generate the next voucher number."),
                                0);
    gnc_register_counter_format_option(odb, counter_section,
                                       N_("Expense voucher number format"),
                                       "gncExpVoucherb",
                                       N_("The format string to use for generating expense voucher numbers. This is a printf-style format string."),
                                       empty_string);
    gnc_register_counter_option(odb, counter_section,
                                N_("Job number"), "gncJoba",
                                N_("The previous job number generated. This number will be incremented to generate the next job number."),
                                0);
    gnc_register_counter_format_option(odb, counter_section,
                                       N_("Job number format"), "gncJobb",
                                       N_("The format string to use for generating job numbers. This is a printf-style format string."),
                                       empty_string);
    gnc_register_counter_option(odb, counter_section,
                                N_("Order number"), "gncOrdera",
                                N_("The previous order number generated. This number will be incremented to generate the next order number."),
                                0);
    gnc_register_counter_format_option(odb, counter_section,
                                       N_("Order number format"), "gncOrderb",
                                       N_("The format string to use for generating order numbers. This is a printf-style format string."),
                                       empty_string);
    gnc_register_counter_option(odb, counter_section,
                                N_("Vendor number"), "gncVendora",
                                N_("The previous vendor number generated. This number will be incremented to generate the next vendor number."),
                                0);
    gnc_register_counter_format_option(odb, counter_section,
                                       N_("Vendor number format"), "gncVendorb",
                                       N_("The format string to use for generating vendor numbers. This is a printf-style format string."),
                                       empty_string);

//Business Tab

    gnc_register_string_option(odb, business_section, N_("Company Name"), "a",
                               N_("The name of your business."),
                               empty_string);
    gnc_register_text_option(odb, business_section, N_("Company Address"), "b1",
                             N_("The address of your business."),
                             empty_string);
    gnc_register_string_option(odb, business_section,
                               N_("Company Contact Person"), "b2",
                               N_("The contact person to print on invoices."),
                               empty_string);
    gnc_register_string_option(odb, business_section,
                               N_("Company Phone Number"), "c1",
                               N_("The contact person to print on invoices."),
                               empty_string);
    gnc_register_string_option(odb, business_section,
                               N_("Company Fax Number"), "c2",
                               N_("The fax number of your business."),
                               empty_string);
    gnc_register_string_option(odb, business_section,
                               N_("Company Email Address"), "c3",
                               N_ ("The email address of your business."),
                               empty_string);
    gnc_register_string_option(odb, business_section,
                               N_("Company Website URL"), "c4",
                               N_("The URL address of your website."),
                               empty_string);
    gnc_register_string_option(odb, business_section, N_("Company ID"), "c5",
                               N_("The ID for your company (eg 'Tax-ID: 00-000000)."),
                               empty_string);
    gnc_register_invoice_print_report_option(odb, business_section,
                                 OPTION_NAME_DEFAULT_INVOICE_REPORT, "e1",
                                 N_("The invoice report to be used for printing."),
                                 empty_string);
    gnc_register_number_range_option<double>(odb, business_section,
                                     OPTION_NAME_DEFAULT_INVOICE_REPORT_TIMEOUT, "e2",
                                     N_("Length of time to change the used invoice report. A value of 0 means disabled."),
                                     0.0, 0.0, 20.0, 1.0);
    gnc_register_taxtable_option(odb, business_section,
                                 N_("Default Customer Tax Table"), "f1",
                                 N_("The default tax table to apply to customers."),
                                 nullptr);
    gnc_register_taxtable_option(odb, business_section,
                                 N_("Default Vendor Tax Table"), "f2",
                                 N_("The default tax table to apply to vendors."),
                                 nullptr);

    gnc_register_dateformat_option(odb, business_section,
                                   N_("Fancy Date Format"), "g",
                                   N_("The default date format used for fancy printed dates."),
                                   {QOF_DATE_FORMAT_UNSET, GNCDATE_MONTH_NUMBER, true, ""});

//Tax Tab

    gnc_register_string_option(odb, N_("Tax"), N_("Tax Number"), "a",
                               N_("The electronic tax number of your business"),
                               empty_string);
}
const QofInstance*
gnc_option_db_lookup_qofinstance_value(GncOptionDB* odb, const char* section,
                                       const char* name)
{
    auto option{odb->find_option(section, name)};
    if (option)
        return option->get_value<const QofInstance*>();
    else
        return nullptr;
}

// Force creation of templates
template void gnc_register_number_range_option(GncOptionDB* db,
                                      const char* section, const char* name,
                                      const char* key, const char* doc_string,
                                      int value, int min, int max, int step);
template void gnc_register_number_range_option(GncOptionDB* db,
                                      const char* section, const char* name,
                                      const char* key, const char* doc_string,
                                      double value, double min,
                                      double max, double step);
