/***********************************************************************\
 * gnc-sql-backend.hpp: Qof Backend for SQL Databases                  *
 *                                                                     *
 * Copyright 2016 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                      *
\***********************************************************************/

#ifndef __GNC_SQL_BACKEND_HPP__
#define __GNC_SQL_BACKEND_HPP__

#include <qof.h>
#include <Account.h>

#include <memory>
#include <exception>
#include <sstream>
#include <vector>
#include <qof-backend.hpp>

class GncSqlColumnTableEntry;
using GncSqlColumnTableEntryPtr = std::shared_ptr<GncSqlColumnTableEntry>;
using EntryVec = std::vector<GncSqlColumnTableEntryPtr>;
class GncSqlObjectBackend;
using GncSqlObjectBackendPtr = std::shared_ptr<GncSqlObjectBackend>;
using OBEEntry = std::tuple<std::string, GncSqlObjectBackendPtr>;
using OBEVec = std::vector<OBEEntry>;
class GncSqlConnection;
class GncSqlStatement;
using GncSqlStatementPtr = std::unique_ptr<GncSqlStatement>;
class GncSqlResult;
using GncSqlResultPtr = GncSqlResult*;
using VersionPair = std::pair<const std::string, unsigned int>;
using VersionVec = std::vector<VersionPair>;
using uint_t = unsigned int;

typedef enum
{
    OP_DB_INSERT,
    OP_DB_UPDATE,
    OP_DB_DELETE
} E_DB_OPERATION;

/**
 *
 * Main SQL backend structure.
 */
class GncSqlBackend : public QofBackend
{
public:
    GncSqlBackend(GncSqlConnection *conn, QofBook* book);
    virtual ~GncSqlBackend();
    /**
     * Load the contents of an SQL database into a book.
     *
     * @param book Book to be loaded
     */
    void load(QofBook*, QofBackendLoadType) override;
    /**
     * Save the contents of a book to an SQL database.
     *
     * @param book Book to be saved
     */
    void sync(QofBook*) override;
    /**
     * An object is about to be edited.
     *
     * @param inst Object being edited
     */
    void begin(QofInstance*) override;
    /**
     * Object editing is complete and the object should be saved.
     *
     * @param inst Object being edited
     */
    void commit(QofInstance*) override;
    /**
     * Object editing has been cancelled.
     *
     * @param inst Object being edited
     */
    void rollback(QofInstance*) override;
    /** Connect the backend to a GncSqlConnection.
     * Sets up version info. Calling with nullptr clears the connection and
     * destroys the version info.
     */
    void connect(GncSqlConnection *conn) noexcept;
    /**
     * Initializes DB table version information.
     */
    void init_version_info() noexcept;
    bool reset_version_info() noexcept;
    /**
     * Finalizes DB table version information.
     */
    void finalize_version_info() noexcept;
    /* FIXME: These are just pass-throughs of m_conn functions. */
    GncSqlStatementPtr create_statement_from_sql(const std::string& str) const noexcept;
    /** Executes an SQL SELECT statement and returns the result rows.  If an
     * error occurs, an entry is added to the log, an error status is returned
     * to qof and nullptr is returned.
     *
     * @param statement Statement
     * @return Results, or nullptr if an error has occurred
     */
    GncSqlResultPtr execute_select_statement(const GncSqlStatementPtr& stmt) const noexcept;
    int execute_nonselect_statement(const GncSqlStatementPtr& stmt) const noexcept;
    std::string quote_string(const std::string&) const noexcept;
    /**
     * Creates a table in the database
     *
     * @param table_name Table name
     * @param col_table DB table description
     * @return TRUE if successful, FALSE if unsuccessful
     */
    bool create_table(const std::string& table_name, const EntryVec& col_table) const noexcept;
    /**
     * Creates a table in the database and sets its version
     *
     * @param table_name Table name
     * @param table_version Table version
     * @param col_table DB table description
     * @return TRUE if successful, FALSE if unsuccessful
     */
    bool create_table(const std::string& table_name, int table_version,
                      const EntryVec& col_table) noexcept;
    /**
     * Create/update all tables in the database
     */
    void create_tables() noexcept;

    /**
     * Creates an index in the database
     *
     * @param index_name Index name
     * @param table_name Table name
     * @param col_table Columns that the index should index
     * @return TRUE if successful, FALSE if unsuccessful
     */
    bool create_index(const std::string& index_name,
                      const std::string& table_name,
                      const EntryVec& col_table) const noexcept;
    /**
     * Adds one or more columns to an existing table.
     *
     * @param table_name SQL table name
     * @param new_col_table Column table for new columns
     * @return TRUE if successful, FALSE if unsuccessful
     */
    bool add_columns_to_table(const std::string& table_name,
                              const EntryVec& col_table) const noexcept;
    /**
     * Upgrades a table to a new structure.
     *
     * The upgrade is done by creating a new table with the new structure,
     * SELECTing the old data into the new table, deleting the old table, then
     * renaming the new table.  Therefore, this will only work if the new table
     * structure is similar enough to the old table that the SELECT will work.
     *
     * @param table_name SQL table name
     * @param col_table Column table
     */
    void upgrade_table (const std::string& table_name,
                        const EntryVec& col_table) noexcept;
    /**
     * Returns the version number for a DB table.
     *
     * @param table_name Table name
     * @return Version number, or 0 if the table does not exist
     */
    uint_t get_table_version(const std::string& table_name) const noexcept;
    bool set_table_version (const std::string& table_name, uint_t version) noexcept;
    /**
     * Register a commodity to be committed after loading is complete.
     *
     * Necessary to save corrections made while loading.
     * @param comm The commodity item to be committed.
     */
    void commodity_for_postload_processing(gnc_commodity*);
    /**
     * Get the GncSqlObjectBackend for the indicated type.
     *
     * Required because we need to pass a pointer to this to a callback via a C
     * function.
     * @param type: The QofInstance type constant to select the object backend.
     */
    GncSqlObjectBackendPtr get_object_backend(const std::string& type) const noexcept;
    /**
     * Checks whether an object is in the database or not.
     *
     * @param table_name DB table name
     * @param obj_name QOF object type name
     * @param pObject Object to be checked
     * @param table DB table description
     * @return TRUE if the object is in the database, FALSE otherwise
     */
    bool object_in_db (const char* table_name, QofIdTypeConst obj_name,
                       const gpointer pObject, const EntryVec& table ) const noexcept;
    /**
     * Performs an operation on the database.
     *
     * @param op Operation type
     * @param table_name SQL table name
     * @param obj_name QOF object type name
     * @param pObject Gnucash object
     * @param table DB table description
     * @return TRUE if successful, FALSE if not
     */
    bool do_db_operation (E_DB_OPERATION op, const char* table_name,
                          QofIdTypeConst obj_name, gpointer pObject,
                          const EntryVec& table) const noexcept;
    /**
     * Ensure that a commodity referenced in another object is in fact saved
     * in the database.
     *
     * @param comm The commodity in question
     * @return true if the commodity needed to be saved.
     */
    bool save_commodity(gnc_commodity* comm) noexcept;
    QofBook* book() const noexcept { return m_book; }
    void set_loading(bool loading) noexcept { m_loading = loading; }
    bool pristine() const noexcept { return m_is_pristine_db; }
    void update_progress(double pct) const noexcept;
    void finish_progress() const noexcept;

protected:
    GncSqlConnection* m_conn = nullptr;  /**< SQL connection */
    QofBook* m_book = nullptr;           /**< The primary, main open book */
    bool m_loading;        /**< We are performing an initial load */
    bool m_in_query;       /**< We are processing a query */
    bool m_is_pristine_db; /**< Are we saving to a new pristine db? */
    const char* m_time_format = nullptr; /**< Server-specific date-time string format */
    VersionVec m_versions;    /**< Version number for each table */
private:
    bool write_account_tree(Account*);
    bool write_accounts();
    bool write_transactions();
    bool write_template_transactions();
    bool write_schedXactions();
    GncSqlStatementPtr build_insert_statement (const char* table_name,
                                               QofIdTypeConst obj_name,
                                               gpointer pObject,
                                               const EntryVec& table) const noexcept;
    GncSqlStatementPtr build_update_statement (const gchar* table_name,
                                               QofIdTypeConst obj_name,
                                               gpointer pObject,
                                               const EntryVec& table) const noexcept;
    GncSqlStatementPtr build_delete_statement (const char* table_name,
                                               QofIdTypeConst obj_name,
                                               gpointer pObject,
                                               const EntryVec& table) const noexcept;

    class ObjectBackendRegistry
    {
    public:
        ObjectBackendRegistry();
        ObjectBackendRegistry(const ObjectBackendRegistry&) = delete;
        ObjectBackendRegistry(const ObjectBackendRegistry&&) = delete;
        ObjectBackendRegistry operator=(const ObjectBackendRegistry&) = delete;
        ObjectBackendRegistry operator=(const ObjectBackendRegistry&&) = delete;
        ~ObjectBackendRegistry() = default;
        void register_backend(OBEEntry&& entry) noexcept;
        void register_backend(GncSqlObjectBackendPtr obe) noexcept;
        GncSqlObjectBackendPtr get_object_backend(const std::string& type) const;
        void load_remaining(GncSqlBackend*);
        OBEVec::iterator begin() { return m_registry.begin(); }
        OBEVec::iterator end() { return m_registry.end(); }
        OBEVec::size_type size() { return m_registry.size(); }
    private:
        OBEVec m_registry;
    };
    ObjectBackendRegistry m_backend_registry;
    std::vector<gnc_commodity*> m_postload_commodities;
};

#endif //__GNC_SQL_BACKEND_HPP__
