#include "DatabaseConnectionPool.h"

#include <thread>
#include <chrono>

// required for MYSQL error constants
#include <errmsg.h>

#include "Utils.h"

DatabaseConnectionPool::DbConnection::DbConnection(Tango::DeviceImpl *dev) :
    Tango::LogAdapter(dev),
    db(mysql_init(nullptr))
{
}

DatabaseConnectionPool::DbConnection::~DbConnection()
{
    auto thread_id = mysql_thread_id(db);
    mysql_kill(db, thread_id);
    mysql_close(db);
}

void DatabaseConnectionPool::DbConnection::connect(const SecureStringPtr &mysql_user,
                                                   const SecureStringPtr &mysql_password,
                                                   const std::string &mysql_host,
                                                   int port,
                                                   const std::string &mysql_db_name)
{
    // apply settings
    mysql_options(db, MYSQL_READ_DEFAULT_GROUP, "client");

    auto null_on_empty = [](const auto &str) -> const char *
    {
        if(str.empty())
        {
            return nullptr;
        }

        return str.c_str();
    };

    auto ret = mysql_real_connect(db,
                                  null_on_empty(mysql_host),
                                  null_on_empty(*mysql_user),
                                  null_on_empty(*mysql_password),
                                  null_on_empty(mysql_db_name),
                                  port,
                                  nullptr,
                                  CLIENT_MULTI_STATEMENTS | CLIENT_FOUND_ROWS);

    if(ret == nullptr)
    {
        WARN_STREAM << "Throw exception because no MySQL connection" << std::endl;
        TangoSys_MemStream out_stream;
        out_stream << "Failed to connect to TANGO database \"" << mysql_db_name << "\" (error = " << mysql_error(db)
                   << ")" << std::ends;

        Tango::Except::throw_exception(
            "CANNOT_CONNECT_MYSQL", out_stream.str(), "DatabaseConnectionPool::init_device()");
    }
}

int DatabaseConnectionPool::conn_pool_size;

DatabaseConnectionPool::DatabaseConnectionPool(Tango::DeviceImpl *dev) :
    Tango::LogAdapter(dev),
    m_dev(dev)
{
    //
    // Get user environment variables if defined
    //
    auto *au = Tango::ApiUtil::instance();

    auto fill_secure_string = [au](const char *env) -> SecureStringPtr
    {
        std::string str;
        au->get_env_var(env, str);

        return std::make_unique<SecureString>(str);
    };

    m_mysql_user = fill_secure_string("MYSQL_USER");
    m_mysql_password = fill_secure_string("MYSQL_PASSWORD");

    std::string mysql_host_with_port, db_name;

    au->get_env_var("MYSQL_HOST", mysql_host_with_port);

    au->get_env_var("MYSQL_DATABASE", db_name);

    if(!db_name.empty())
    {
        // override TANGO_DB_NAME
        m_mysql_db_name = db_name;
    }

    parse_port_from_host(mysql_host_with_port, m_mysql_port, m_mysql_host);

    create_connection_pool();
}

void DatabaseConnectionPool::parse_port_from_host(const std::string &mysql_host,
                                                  int &port,
                                                  std::string &host_without_port)
{
    port = 0;
    host_without_port = "";

    if(!mysql_host.empty())
    {
        WARN_STREAM << "DatabaseConnectionPool::parse_port_from_host(): mysql host = " << mysql_host << std::endl;
        std::string::size_type pos = mysql_host.find(':');
        if(pos != std::string::npos)
        {
            host_without_port = mysql_host.substr(0, pos);
            pos++;
            std::string port_str = mysql_host.substr(pos);

            try
            {
                port = ParseString<int>(port_str.c_str());
            }
            catch(Tango::DevFailed &)
            {
                // do nothing
            }
        }
        else
        {
            host_without_port = mysql_host;
        }
        WARN_STREAM << "DatabaseConnectionPool::parse_port_from_host(): mysql host = " << host_without_port
                    << ", port = " << port << std::endl;
    }
}

//+------------------------------------------------------------------
/**
 *	method:	create_connection_pool()
 *
 *	description:	Create the MySQL connections pool
 *
 */
//+------------------------------------------------------------------

void DatabaseConnectionPool::create_connection_pool()
{
    for(int loop = 0; loop < conn_pool_size; loop++)
    {
        m_conn_pool.emplace_back(std::make_unique<DbConnection>(m_dev));

        WARN_STREAM << "Going to connect to MySQL for connection " << loop << std::endl;
        m_conn_pool[loop]->connect(m_mysql_user, m_mysql_password, m_mysql_host, m_mysql_port, m_mysql_db_name);
    }
}

//+----------------------------------------------------------------------------
//
// method :            DatabaseConnectionPool::get_connection()
//
// description :       Get a MySQl connection from the connection pool
//
//-----------------------------------------------------------------------------
int DatabaseConnectionPool::get_connection()
{
    //
    // Get a MySQL connection and lock it
    // If none available, wait for one
    //

    int loop = 0;
    while(m_conn_pool[loop]->the_sema.trywait() == 0)
    {
        loop++;
        if(loop == conn_pool_size)
        {
            int sem_to_wait;
            {
                omni_mutex_lock oml(m_sem_wait_mutex);
                sem_to_wait = m_last_sem_wait++;
                if(m_last_sem_wait == conn_pool_size)
                {
                    m_last_sem_wait = 0;
                }
            }
            loop = sem_to_wait;
            WARN_STREAM << "Waiting for one free MySQL connection on semaphore " << loop << std::endl;
            m_conn_pool[loop]->the_sema.wait();
            break;
        }
    }

    return loop;
}

void DatabaseConnectionPool::release_connection(int con_nb)
{
    m_conn_pool[con_nb]->the_sema.post();
}

MYSQL *DatabaseConnectionPool::GetDatabase(int con_nb)
{
    DEBUG_STREAM << "Using MySQL connection with semaphore " << con_nb << std::endl;

    auto err = mysql_ping(m_conn_pool[con_nb]->db);
    if(err)
    {
        ERROR_STREAM << "DatabaseConnectionPool::GetDatabase: Error pinging the database with connection " << con_nb
                     << " with " << mysql_error(m_conn_pool[con_nb]->db) << std::endl;
        ERROR_STREAM << "Trying reconnection now ..." << std::endl;
        m_conn_pool[con_nb] = std::make_unique<DbConnection>(m_dev);
        m_conn_pool[con_nb]->connect(m_mysql_user, m_mysql_password, m_mysql_host, m_mysql_port, m_mysql_db_name);
    }

    return m_conn_pool[con_nb]->db;
}

void DatabaseConnectionPool::set_conn_pool_size(int si)
{
    conn_pool_size = si;
}
