/********************************************************************\
 * qofsesssion.cpp -- session access (connection to backend)        *
 *                                                                  *
 * 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                   *
\********************************************************************/

/**
 * @file qofsession.cpp
 * @brief Encapsulate a connection to a storage backend.
 *
 * HISTORY:
 * Created by Linas Vepstas December 1998

 @author Copyright (c) 1998-2004 Linas Vepstas <linas@linas.org>
 @author Copyright (c) 2000 Dave Peticolas
 @author Copyright (c) 2005 Neil Williams <linux@codehelp.co.uk>
 @author Copyright (c) 2016 Aaron Laws
   */

extern "C"
{

#include <config.h>

#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#ifdef HAVE_UNISTD_H
# include <unistd.h>
#else
# ifdef __GNUC__
#  warning "<unistd.h> required."
# endif
#endif

#include <glib.h>
#include "qof.h"
#include "qofobject-p.h"

static QofLogModule log_module = QOF_MOD_SESSION;
} //extern 'C'

#include "qofbook-p.h"
#include "qof-backend.hpp"
#include "qofsession.hpp"
#include "gnc-backend-prov.hpp"

#include <vector>
#include <boost/algorithm/string.hpp>
#include <vector>
#include <algorithm>
#include <string>
#include <sstream>

using ProviderVec =  std::vector<QofBackendProvider_ptr>;
static ProviderVec s_providers;
static const std::string empty_string{};
/*
 * These getters are used in tests to reach static vars from outside
 * They should be removed when no longer needed
 */

ProviderVec& get_providers (void );
bool get_providers_initialized (void );

ProviderVec&
get_providers (void)
{
    return s_providers;
}

bool
get_providers_initialized (void)
{
    return !s_providers.empty();
}

void
qof_backend_register_provider (QofBackendProvider_ptr&& prov)
{
    s_providers.emplace_back(std::move(prov));
}

void
qof_backend_unregister_all_providers ()
{
    s_providers.clear ();
}

/* Called from C so we have to keep the GList for now. */
GList*
qof_backend_get_registered_access_method_list(void)
{
    GList* list = NULL;
    std::for_each(s_providers.begin(), s_providers.end(),
                  [&list](QofBackendProvider_ptr& provider) {
                      gpointer method = reinterpret_cast<gpointer>(const_cast<char*>(provider->access_method));
                      list = g_list_prepend(list, method);
                  });
    return list;
}

/* QofSessionImpl */
/* ====================================================================== */
/* Constructor/Destructor ----------------------------------*/

QofSessionImpl::QofSessionImpl (QofBook* book) noexcept
  : m_backend {},
    m_book {book},
    m_uri {},
    m_saving {false},
    m_last_err {},
    m_error_message {}
{
}

QofSessionImpl::~QofSessionImpl () noexcept
{
    ENTER ("sess=%p uri=%s", this, m_uri.c_str ());
    end ();
    destroy_backend ();
    qof_book_set_backend (m_book, nullptr);
    qof_book_destroy (m_book);
    m_book = nullptr;
    LEAVE ("sess=%p", this);
}

void
qof_session_destroy (QofSession * session)
{
    delete session;
}

QofSession *
qof_session_new (QofBook* book)
{
    return new QofSessionImpl(book);
}

void
QofSessionImpl::destroy_backend () noexcept
{
    if (m_backend)
    {
        clear_error ();
        delete m_backend;
        m_backend = nullptr;
        qof_book_set_backend (m_book, nullptr);
    }
}

/* ====================================================================== */

void
QofSessionImpl::load_backend (std::string access_method) noexcept
{
    std::ostringstream s;
    s << " list=" << s_providers.size();
    ENTER ("%s", s.str().c_str());
    for (auto const & prov : s_providers)
    {
        if (!boost::iequals (access_method, prov->access_method))
        {
            PINFO ("The provider providers access_method, %s, but we're loading for access_method, %s. Skipping.",
                    prov->access_method, access_method.c_str ());
            continue;
        }
        PINFO (" Selected provider %s", prov->provider_name);
        // Only do a type check when trying to open an existing file
        // When saving over an existing file the contents of the original file don't matter
        if (!m_creating && !prov->type_check (m_uri.c_str ()))
        {
            PINFO("Provider, %s, reported not being usable for book, %s.",
                    prov->provider_name, m_uri.c_str ());
            continue;
        }
        m_backend = prov->create_backend();
        LEAVE (" ");
        return;
    }
    std::string msg {"failed to get_backend using access method \"" + access_method + "\""};
    push_error (ERR_BACKEND_NO_HANDLER, msg);
    LEAVE (" ");
}

void
QofSessionImpl::load (QofPercentageFunc percentage_func) noexcept
{
    /* We must have an empty book to load into or bad things will happen. */
    g_return_if_fail(m_book && qof_book_empty(m_book));

    if (!m_uri.size ()) return;
    ENTER ("sess=%p uri=%s", this, m_uri.c_str ());

    /* At this point, we should are supposed to have a valid book
     * id and a lock on the file. */
    clear_error ();

    /* This code should be sufficient to initialize *any* backend,
     * whether http, postgres, or anything else that might come along.
     * Basically, the idea is that by now, a backend has already been
     * created & set up.  At this point, we only need to get the
     * top-level account group out of the backend, and that is a
     * generic, backend-independent operation.
     */
    qof_book_set_backend (m_book, m_backend);

    /* Starting the session should result in a bunch of accounts
     * and currencies being downloaded, but probably no transactions;
     * The GUI will need to do a query for that.
     */
    if (m_backend)
    {
        m_backend->set_percentage(percentage_func);
        m_backend->load (m_book, LOAD_TYPE_INITIAL_LOAD);
        push_error (m_backend->get_error(), {});
    }

    auto err = get_error ();
    if ((err != ERR_BACKEND_NO_ERR) &&
        (err != ERR_FILEIO_FILE_TOO_OLD) &&
        (err != ERR_FILEIO_NO_ENCODING) &&
        (err != ERR_FILEIO_FILE_UPGRADE) &&
        (err != ERR_SQL_DB_TOO_OLD) &&
        (err != ERR_SQL_DB_TOO_NEW))
    {
        // Something failed, delete and restore new ones.
        destroy_backend();
        qof_book_destroy (m_book);
        m_book = qof_book_new();
        LEAVE ("error from backend %d", get_error ());
        return;
    }

    LEAVE ("sess = %p, uri=%s", this, m_uri.c_str ());
}

void
QofSessionImpl::begin (const char* new_uri, SessionOpenMode mode) noexcept
{


    ENTER (" sess=%p mode=%d, URI=%s", this, mode, new_uri);
    clear_error ();
    /* Check to see if this session is already open */
    if (m_uri.size ())
    {
        if (ERR_BACKEND_NO_ERR != get_error ())
            push_error (ERR_BACKEND_LOCKED, {});
        LEAVE("push error book is already open ");
        return;
    }

    /* seriously invalid */
    if (!new_uri)
    {
        if (ERR_BACKEND_NO_ERR != get_error ())
            push_error (ERR_BACKEND_BAD_URL, {});
        LEAVE("push error missing new_uri");
        return;
    }

    char * scheme {g_uri_parse_scheme (new_uri)};
    char * filename {nullptr};
    if (g_strcmp0 (scheme, "file") == 0)
        filename = g_filename_from_uri (new_uri, nullptr, nullptr);
    else if (!scheme)
        filename = g_strdup (new_uri);

    if (filename && g_file_test (filename, G_FILE_TEST_IS_DIR))
    {
        if (ERR_BACKEND_NO_ERR == get_error ())
            push_error (ERR_BACKEND_BAD_URL, {});
        g_free (filename);
        g_free (scheme);
        LEAVE("Can't open a directory");
        return;
    }
    /* destroy the old backend */
    destroy_backend ();
    /* Store the session URL  */
    m_uri = new_uri;
    m_creating = mode == SESSION_NEW_STORE || mode == SESSION_NEW_OVERWRITE;
    if (filename)
        load_backend ("file");
    else                       /* access method found, load appropriate backend */
        load_backend (scheme);
    g_free (filename);
    g_free (scheme);

    /* No backend was found. That's bad. */
    if (m_backend == nullptr)
    {
        m_uri = {};
        if (ERR_BACKEND_NO_ERR == get_error ())
            push_error (ERR_BACKEND_BAD_URL, {});
        LEAVE (" BAD: no backend: sess=%p book-id=%s",
               this,  new_uri);
        return;
    }

    /* If there's a begin method, call that. */
    m_backend->session_begin(this, m_uri.c_str(), mode);
    PINFO ("Done running session_begin on backend");
    QofBackendError const err {m_backend->get_error()};
    auto msg (m_backend->get_message());
    if (err != ERR_BACKEND_NO_ERR)
    {
        m_uri = {};
        push_error (err, msg);
        LEAVE (" backend error %d %s", err, msg.empty() ? "(null)" : msg.c_str());
        return;
    }
    if (!msg.empty())
    {
        PWARN("%s", msg.c_str());
    }

    LEAVE (" sess=%p book-id=%s", this,  new_uri);
}

void
QofSessionImpl::end () noexcept
{
    ENTER ("sess=%p uri=%s", this, m_uri.c_str ());
    auto backend = qof_book_get_backend (m_book);
    if (backend != nullptr)
        backend->session_end();
    clear_error ();
    m_uri.clear();
    LEAVE ("sess=%p uri=%s", this, m_uri.c_str ());
}

/* error handling functions --------------------------------*/

void
QofSessionImpl::clear_error () noexcept
{
    m_last_err = ERR_BACKEND_NO_ERR;
    m_error_message = {};

    /* pop the stack on the backend as well. */
    if (auto backend = qof_book_get_backend (m_book))
    {
        QofBackendError err = ERR_BACKEND_NO_ERR;
        do
            err = backend->get_error();
        while (err != ERR_BACKEND_NO_ERR);
    }
}

void
QofSessionImpl::push_error (QofBackendError const err, std::string message) noexcept
{
    m_last_err = err;
    m_error_message = message;
}

QofBackendError
QofSessionImpl::get_error () noexcept
{
    /* if we have a local error, return that. */
    if (m_last_err != ERR_BACKEND_NO_ERR)
        return m_last_err;
    auto qof_be = qof_book_get_backend (m_book);
    if (qof_be == nullptr) return ERR_BACKEND_NO_ERR;

    m_last_err = qof_be->get_error();
    return m_last_err;
}

const std::string&
QofSessionImpl::get_error_message () const noexcept
{
    return m_error_message;
}

QofBackendError
QofSessionImpl::pop_error () noexcept
{
    QofBackendError err {get_error ()};
    clear_error ();
    return err;
}

/* Accessors (getters/setters) -----------------------------*/

QofBook *
QofSessionImpl::get_book () const noexcept
{
    if (!m_book) return nullptr;
    if ('y' == m_book->book_open)
        return m_book;
    return nullptr;
}

QofBackend *
QofSession::get_backend () const noexcept
{
    return m_backend;
}

const std::string&
QofSessionImpl::get_file_path () const noexcept
{
    auto backend = qof_book_get_backend (m_book);
    if (!backend) return empty_string;
    return backend->get_uri();
}

std::string const &
QofSessionImpl::get_uri () const noexcept
{
    return m_uri;
}

bool
QofSessionImpl::is_saving () const noexcept
{
    return m_saving;
}

/* Manipulators (save, load, etc.) -------------------------*/

void
QofSessionImpl::save (QofPercentageFunc percentage_func) noexcept
{
    if (!qof_book_session_not_saved (m_book)) //Clean book, nothing to do.
        return;
    m_saving = true;
    ENTER ("sess=%p uri=%s", this, m_uri.c_str ());

    /* If there is a backend, the book is dirty, and the backend is reachable
     * (i.e. we can communicate with it), then synchronize with the backend.  If
     * we cannot contact the backend (e.g.  because we've gone offline, the
     * network has crashed, etc.)  then raise an error so that the controlling
     * dialog can offer the user a chance to save in a different way.
     */
    if (m_backend)
    {
        /* if invoked as SaveAs(), then backend not yet set */
        if (qof_book_get_backend (m_book) != m_backend)
            qof_book_set_backend (m_book, m_backend);
        m_backend->set_percentage(percentage_func);
        m_backend->sync(m_book);
        auto err = m_backend->get_error();
        if (err != ERR_BACKEND_NO_ERR)
        {
            push_error (err, {});
            m_saving = false;
            return;
        }
        /* If we got to here, then the backend saved everything
        * just fine, and we are done. So return. */
        clear_error ();
        LEAVE("Success");
    }
    else
    {
        push_error (ERR_BACKEND_NO_HANDLER, "failed to load backend");
        LEAVE("error -- No backend!");
    }
    m_saving = false;
}

void
QofSessionImpl::safe_save (QofPercentageFunc percentage_func) noexcept
{
    if (!(m_backend && m_book)) return;
    if (qof_book_get_backend (m_book) != m_backend)
        qof_book_set_backend (m_book, m_backend);
    m_backend->set_percentage(percentage_func);
    m_backend->safe_sync(get_book ());
    auto err = m_backend->get_error();
    auto msg = m_backend->get_message();
    if (err != ERR_BACKEND_NO_ERR)
    {
        m_uri = "";
        push_error (err, msg);
    }
}

void
QofSessionImpl::ensure_all_data_loaded () noexcept
{
    if (!(m_backend && m_book)) return;
    if (qof_book_get_backend (m_book) != m_backend)
        qof_book_set_backend (m_book, m_backend);
    m_backend->load(m_book, LOAD_TYPE_LOAD_ALL);
    push_error (m_backend->get_error(), {});
}

void
QofSessionImpl::swap_books (QofSessionImpl & other) noexcept
{
    ENTER ("sess1=%p sess2=%p", this, &other);
    // don't swap (that is, double-swap) read_only flags
    if (m_book && other.m_book)
        std::swap (m_book->read_only, other.m_book->read_only);
    std::swap (m_book, other.m_book);
    auto mybackend = qof_book_get_backend (m_book);
    qof_book_set_backend (m_book, qof_book_get_backend (other.m_book));
    qof_book_set_backend (other.m_book, mybackend);
    LEAVE (" ");
}

bool
QofSessionImpl::events_pending () const noexcept
{
    return false;
}

bool
QofSessionImpl::process_events () const noexcept
{
    return false;
}

/* XXX This exports the list of accounts to a file.  It does not
 * export any transactions.  It's a place-holder until full
 * book-closing is implemented.
 */
bool
QofSessionImpl::export_session (QofSessionImpl & real_session,
                                QofPercentageFunc percentage_func) noexcept
{
    auto real_book = real_session.get_book ();
    ENTER ("tmp_session=%p real_session=%p book=%p uri=%s",
           this, &real_session, real_book, m_uri.c_str ());

    /* There must be a backend or else.  (It should always be the file
     * backend too.)
     */
    if (!m_backend) return false;

    m_backend->set_percentage(percentage_func);

    m_backend->export_coa(real_book);
    auto err = m_backend->get_error();
    if (err != ERR_BACKEND_NO_ERR)
        return false;
    return true;
}

/* C Wrapper Functions */
/* ====================================================================== */

const char *
qof_session_get_error_message (const QofSession * session)
{
    if (!session) return "";
    return session->get_error_message ().c_str ();
}

QofBackendError
qof_session_pop_error (QofSession * session)
{
    if (!session) return ERR_BACKEND_NO_BACKEND;
    return session->pop_error ();
}

QofBook *
qof_session_get_book (const QofSession *session)
{
    if (!session) return NULL;
    return session->get_book ();
}

const char *
qof_session_get_file_path (const QofSession *session)
{
    if (!session) return nullptr;
    auto& path{session->get_file_path()};
    return path.empty() ? nullptr : path.c_str ();
}

void
qof_session_ensure_all_data_loaded (QofSession *session)
{
    if (session == nullptr) return;
    return session->ensure_all_data_loaded ();
}

const char *
qof_session_get_url (const QofSession *session)
{
    if (!session) return NULL;
    return session->get_uri ().c_str ();
}

QofBackend *
qof_session_get_backend (const QofSession *session)
{
    if (!session) return NULL;
    return session->get_backend ();
}

void
qof_session_begin (QofSession *session, const char * uri, SessionOpenMode mode)
{
    if (!session) return;
    session->begin(uri, mode);
}

void
qof_session_load (QofSession *session,
                  QofPercentageFunc percentage_func)
{
    if (!session) return;
    session->load (percentage_func);
}

void
qof_session_save (QofSession *session,
                  QofPercentageFunc percentage_func)
{
    if (!session) return;
    session->save (percentage_func);
}

void
qof_session_safe_save(QofSession *session, QofPercentageFunc percentage_func)
{
    if (!session) return;
    session->safe_save (percentage_func);
}

gboolean
qof_session_save_in_progress(const QofSession *session)
{
    if (!session) return false;
    return session->is_saving ();
}

void
qof_session_end (QofSession *session)
{
    if (!session) return;
    session->end ();
}

void
qof_session_swap_data (QofSession *session_1, QofSession *session_2)
{
    if (session_1 == session_2) return;
    if (!session_1 || !session_2) return;
    session_1->swap_books (*session_2);
}

gboolean
qof_session_events_pending (const QofSession *session)
{
    if (!session) return false;
    return session->events_pending ();
}

gboolean
qof_session_process_events (QofSession *session)
{
    if (!session) return FALSE;
    return session->process_events ();
}

gboolean
qof_session_export (QofSession *tmp_session,
                    QofSession *real_session,
                    QofPercentageFunc percentage_func)
{
    if ((!tmp_session) || (!real_session)) return FALSE;
    return tmp_session->export_session (*real_session, percentage_func);
}

/* ================= Static function access for testing ================= */

void init_static_qofsession_pointers (void);

void qof_session_load_backend (QofSession * session, const char * access_method)
{
    session->load_backend (access_method);
}

static void
qof_session_clear_error (QofSession * session)
{
    session->clear_error ();
}

static void
qof_session_destroy_backend (QofSession * session)
{
    session->destroy_backend ();
}

void qof_session_set_uri (QofSession * session, char const * uri)
{
    if (!uri)
        session->m_uri = "";
    else
        session->m_uri = uri;
}

void (*p_qof_session_load_backend) (QofSession *, const char * access_method);
void (*p_qof_session_clear_error) (QofSession *);
void (*p_qof_session_destroy_backend) (QofSession *);
void (*p_qof_session_set_uri) (QofSession *, char const * uri);

void
init_static_qofsession_pointers (void)
{
    p_qof_session_load_backend = &qof_session_load_backend;
    p_qof_session_clear_error = &qof_session_clear_error;
    p_qof_session_destroy_backend = &qof_session_destroy_backend;
    p_qof_session_set_uri = &qof_session_set_uri;
}

QofBackendError
qof_session_get_error (QofSession * session)
{
    if (!session) return ERR_BACKEND_NO_BACKEND;
    return session->get_error();
}
