/**
 * MaNGOS is a full featured server for World of Warcraft, supporting
 * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8
 *
 * Copyright (C) 2005-2022 MaNGOS <https://getmangos.eu>
 *
 * 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * World of Warcraft, and all World of Warcraft or Warcraft art, images,
 * and lore are copyrighted by Blizzard Entertainment, Inc.
 */

#include "DatabaseEnv.h"
#include "Config/Config.h"
#include "Database/SqlOperations.h"
#include "GitRevision.h"

#include <ctime>
#include <iostream>
#include <fstream>
#include <memory>

#define MIN_CONNECTION_POOL_SIZE 1
#define MAX_CONNECTION_POOL_SIZE 16

struct DBVersion
{
    std::string dbname;
    std::string expected_version;
    std::string expected_structure;
    std::string minimal_expected_content; // Minimal because core can starts with some missing contents
    std::string description;
};

const DBVersion databaseVersions[COUNT_DATABASES] = {
    { "World", GitRevision::GetWorldDBVersion(), GitRevision::GetWorldDBStructure(), GitRevision::GetWorldDBContent(), GitRevision::GetWorldDBUpdateDescription() }, // DATABASE_WORLD
    { "Realmd", GitRevision::GetRealmDBVersion(), GitRevision::GetRealmDBStructure(), GitRevision::GetRealmDBContent(), GitRevision::GetRealmDBUpdateDescription() }, // DATABASE_REALMD
    { "Character", GitRevision::GetCharDBVersion(), GitRevision::GetCharDBStructure(), GitRevision::GetCharDBContent(), GitRevision::GetCharDBUpdateDescription() }, // DATABASE_CHARACTER
};

//////////////////////////////////////////////////////////////////////////
SqlPreparedStatement* SqlConnection::CreateStatement(const std::string& fmt)
{
    return new SqlPlainPreparedStatement(fmt, *this);
}

void SqlConnection::FreePreparedStatements()
{
    SqlConnection::Lock guard(this);

    size_t nStmts = m_holder.size();
    for (size_t i = 0; i < nStmts; ++i)
    {
        delete m_holder[i];
    }

    m_holder.clear();
}

SqlPreparedStatement* SqlConnection::GetStmt(uint32 nIndex)
{
    // resize stmt container
    if (m_holder.size() <= nIndex)
    {
        m_holder.resize(nIndex + 1, NULL);
    }

    SqlPreparedStatement* pStmt = NULL;

    // create stmt if needed
    if (m_holder[nIndex] == NULL)
    {
        // obtain SQL request string
        std::string fmt = m_db.GetStmtString(nIndex);
        MANGOS_ASSERT(fmt.length());
        // allocate SQlPreparedStatement object
        pStmt = CreateStatement(fmt);
        // prepare statement
        if (!pStmt->prepare())
        {
            MANGOS_ASSERT(false && "Unable to prepare SQL statement");
            return NULL;
        }

        // save statement in internal registry
        m_holder[nIndex] = pStmt;
    }
    else
    {
        pStmt = m_holder[nIndex];
    }

    return pStmt;
}

bool SqlConnection::ExecuteStmt(int nIndex, const SqlStmtParameters& id)
{
    if (nIndex == -1)
    {
        return false;
    }

    // get prepared statement object
    SqlPreparedStatement* pStmt = GetStmt(nIndex);
    // bind parameters
    pStmt->bind(id);
    // execute statement
    return pStmt->execute();
}

//////////////////////////////////////////////////////////////////////////
Database::~Database()
{
    StopServer();
}

bool Database::Initialize(const char* infoString, int nConns /*= 1*/)
{
    // Enable logging of SQL commands (usually only GM commands)
    // (See method: PExecuteLog)
    m_logSQL = sConfig.GetBoolDefault("LogSQL", false);
    m_logsDir = sConfig.GetStringDefault("LogsDir", "");
    if (!m_logsDir.empty())
    {
        if ((m_logsDir.at(m_logsDir.length() - 1) != '/') && (m_logsDir.at(m_logsDir.length() - 1) != '\\'))
        {
            m_logsDir.append("/");
        }
    }

    m_pingIntervallms = sConfig.GetIntDefault("MaxPingTime", 30) * (MINUTE * 1000);

    // create DB connections

    // setup connection pool size
    if (nConns < MIN_CONNECTION_POOL_SIZE)
    {
        m_nQueryConnPoolSize = MIN_CONNECTION_POOL_SIZE;
    }
    else if (nConns > MAX_CONNECTION_POOL_SIZE)
    {
        m_nQueryConnPoolSize = MAX_CONNECTION_POOL_SIZE;
    }
    else
    {
        m_nQueryConnPoolSize = nConns;
    }

    // create connection pool for sync requests
    for (int i = 0; i < m_nQueryConnPoolSize; ++i)
    {
        SqlConnection* pConn = CreateConnection();
        if (!pConn->Initialize(infoString))
        {
            delete pConn;
            return false;
        }

        m_pQueryConnections.push_back(pConn);
    }

    // create and initialize connection for async requests
    m_pAsyncConn = CreateConnection();
    if (!m_pAsyncConn->Initialize(infoString))
    {
        return false;
    }

    m_pResultQueue = new SqlResultQueue;

    InitDelayThread();
    return true;
}

void Database::StopServer()
{
    HaltDelayThread();

    delete m_pResultQueue;
    delete m_pAsyncConn;

    m_pResultQueue = NULL;
    m_pAsyncConn = NULL;

    for (size_t i = 0; i < m_pQueryConnections.size(); ++i)
    {
        delete m_pQueryConnections[i];
    }

    m_pQueryConnections.clear();
}

SqlDelayThread* Database::CreateDelayThread()
{
    assert(m_pAsyncConn);
    return new SqlDelayThread(this, m_pAsyncConn);
}

void Database::InitDelayThread()
{
    assert(!m_delayThread);

    // New delay thread for delay execute
    m_threadBody = CreateDelayThread();              // will deleted at m_delayThread delete
    m_TransStorage = new ACE_TSS<Database::TransHelper>();
    m_delayThread = new ACE_Based::Thread(m_threadBody);
}

void Database::HaltDelayThread()
{
    if (!m_threadBody || !m_delayThread)
    {
        return;
    }

    m_threadBody->Stop();                                   // Stop event
    m_delayThread->wait();                                  // Wait for flush to DB
    delete m_TransStorage;
    delete m_delayThread;                                   // This also deletes m_threadBody
    m_delayThread = NULL;
    m_threadBody = NULL;
    m_TransStorage=NULL;
}

void Database::ThreadStart()
{
}

void Database::ThreadEnd()
{
}

void Database::ProcessResultQueue()
{
    if (m_pResultQueue)
    {
        m_pResultQueue->Update();
    }
}

void Database::escape_string(std::string& str)
{
    if (str.empty())
    {
        return;
    }

    char* buf = new char[str.size() * 2 + 1];
    // we don't care what connection to use - escape string will be the same
    m_pQueryConnections[0]->escape_string(buf, str.c_str(), str.size());
    str = buf;
    delete[] buf;
}

SqlConnection* Database::getQueryConnection()
{
    int nCount = 0;

    if (m_nQueryCounter == long(1 << 31))
    {
        m_nQueryCounter = 0;
    }
    else
    {
        nCount = ++m_nQueryCounter;
    }

    return m_pQueryConnections[nCount % m_nQueryConnPoolSize];
}

void Database::Ping()
{
    const char* sql = "SELECT 1";

    {
        SqlConnection::Lock guard(m_pAsyncConn);
        delete guard->Query(sql);
    }

    for (int i = 0; i < m_nQueryConnPoolSize; ++i)
    {
        SqlConnection::Lock guard(m_pQueryConnections[i]);
        delete guard->Query(sql);
    }
}

bool Database::PExecuteLog(const char* format, ...)
{
    if (!format)
    {
        return false;
    }

    va_list ap;
    char szQuery [MAX_QUERY_LEN];
    va_start(ap, format);
    int res = vsnprintf(szQuery, MAX_QUERY_LEN, format, ap);
    va_end(ap);

    if (res == -1)
    {
        sLog.outError("SQL Query truncated (and not execute) for format: %s", format);
        return false;
    }

    if (m_logSQL)
    {
        time_t curr;
        tm local;
        time(&curr);                                        // get current time_t value
        local = *(localtime(&curr));                        // dereference and assign
        char fName[128];
        sprintf(fName, "%04d-%02d-%02d_logSQL.sql", local.tm_year + 1900, local.tm_mon + 1, local.tm_mday);

        FILE* log_file;
        std::string logsDir_fname = m_logsDir + fName;
        log_file = fopen(logsDir_fname.c_str(), "a");
        if (log_file)
        {
            fprintf(log_file, "%s;\n", szQuery);
            fclose(log_file);
        }
        else
        {
            // The file could not be opened
            sLog.outError("SQL-Logging is disabled - Log file for the SQL commands could not be openend: %s", fName);
        }
    }

    return Execute(szQuery);
}

QueryResult* Database::PQuery(const char* format, ...)
{
    if (!format)
    {
        return NULL;
    }

    va_list ap;
    char szQuery [MAX_QUERY_LEN];
    va_start(ap, format);
    int res = vsnprintf(szQuery, MAX_QUERY_LEN, format, ap);
    va_end(ap);

    if (res == -1)
    {
        sLog.outError("SQL Query truncated (and not execute) for format: %s", format);
        return NULL;
    }

    return Query(szQuery);
}

QueryNamedResult* Database::PQueryNamed(const char* format, ...)
{
    if (!format)
    {
        return NULL;
    }

    va_list ap;
    char szQuery [MAX_QUERY_LEN];
    va_start(ap, format);
    int res = vsnprintf(szQuery, MAX_QUERY_LEN, format, ap);
    va_end(ap);

    if (res == -1)
    {
        sLog.outError("SQL Query truncated (and not execute) for format: %s", format);
        return NULL;
    }

    return QueryNamed(szQuery);
}

bool Database::Execute(const char* sql)
{
    if (!m_pAsyncConn)
    {
        return false;
    }

    SqlTransaction* pTrans = (*m_TransStorage)->get();
    if (pTrans)
    {
        // add SQL request to trans queue
        pTrans->DelayExecute(new SqlPlainRequest(sql));
    }
    else
    {
        // if async execution is not available
        if (!m_bAllowAsyncTransactions)
        {
            return DirectExecute(sql);
        }

        // Simple sql statement
        m_threadBody->Delay(new SqlPlainRequest(sql));
    }

    return true;
}

bool Database::PExecute(const char* format, ...)
{
    if (!format)
    {
        return false;
    }

    va_list ap;
    char szQuery [MAX_QUERY_LEN];
    va_start(ap, format);
    int res = vsnprintf(szQuery, MAX_QUERY_LEN, format, ap);
    va_end(ap);

    if (res == -1)
    {
        sLog.outError("SQL Query truncated (and not execute) for format: %s", format);
        return false;
    }

    return Execute(szQuery);
}

bool Database::DirectPExecute(const char* format, ...)
{
    if (!format)
    {
        return false;
    }

    va_list ap;
    char szQuery [MAX_QUERY_LEN];
    va_start(ap, format);
    int res = vsnprintf(szQuery, MAX_QUERY_LEN, format, ap);
    va_end(ap);

    if (res == -1)
    {
        sLog.outError("SQL Query truncated (and not execute) for format: %s", format);
        return false;
    }

    return DirectExecute(szQuery);
}

bool Database::BeginTransaction()
{
    if (!m_pAsyncConn)
    {
        return false;
    }

    // initiate transaction on current thread
    // currently we do not support queued transactions
    (*m_TransStorage)->init();
    return true;
}

bool Database::CommitTransaction()
{
    if (!m_pAsyncConn)
    {
        return false;
    }

    // check if we have pending transaction
    if (!(*m_TransStorage)->get())
    {
        return false;
    }

    // if async execution is not available
    if (!m_bAllowAsyncTransactions)
    {
        return CommitTransactionDirect();
    }

    // add SqlTransaction to the async queue
    m_threadBody->Delay((*m_TransStorage)->detach());
    return true;
}

bool Database::CommitTransactionDirect()
{
    if (!m_pAsyncConn)
    {
        return false;
    }

    // check if we have pending transaction
    if (!(*m_TransStorage)->get())
    {
        return false;
    }

    // directly execute SqlTransaction
    SqlTransaction* pTrans = (*m_TransStorage)->detach();
    pTrans->Execute(m_pAsyncConn);
    delete pTrans;

    return true;
}

bool Database::RollbackTransaction()
{
    if (!m_pAsyncConn)
    {
        return false;
    }

    if (!(*m_TransStorage)->get())
    {
        return false;
    }

    // remove scheduled transaction
    (*m_TransStorage)->reset();

    return true;
}

void PrintNormalYouHaveDatabaseVersion(std::string current_db_version, std::string current_db_structure, std::string current_db_content, std::string description)
{
    sLog.outString("  [A] You have database Version: %s", current_db_version.c_str());
    sLog.outString("                      Structure: %s", current_db_structure.c_str());
    sLog.outString("                        Content: %s", current_db_content.c_str());
    sLog.outString("                    Description: %s", description.c_str());
}

void PrintErrorYouHaveDatabaseVersion(std::string current_db_version, std::string current_db_structure, std::string current_db_content, std::string description)
{
    sLog.outErrorDb("  [A] You have database Version: %s", current_db_version.c_str());
    sLog.outErrorDb("                      Structure: %s", current_db_structure.c_str());
    sLog.outErrorDb("                        Content: %s", current_db_content.c_str());
    sLog.outErrorDb("                    Description: %s", description.c_str());
}

void PrintNormalDatabaseVersionReferencedByCore(const DBVersion& core_db_requirements)
{
    sLog.outString("  [B] The core references last database Version: %s", core_db_requirements.expected_version.c_str());
    sLog.outString("                                      Structure: %s", core_db_requirements.expected_structure.c_str());
    sLog.outString("                                        Content: %s", core_db_requirements.minimal_expected_content.c_str());
    sLog.outString("                                    Description: %s", core_db_requirements.description.c_str());
}

void PrintErrorYouNeedDatabaseVersionExpectedByCore(const DBVersion& core_db_requirements)
{
    sLog.outErrorDb("  [B] The core needs database Version: %s", core_db_requirements.expected_version.c_str());
    sLog.outErrorDb("                            Structure: %s", core_db_requirements.expected_structure.c_str());
    sLog.outErrorDb("                              Content: %s", core_db_requirements.minimal_expected_content.c_str());
    sLog.outErrorDb("                          Description: %s", core_db_requirements.description.c_str());
}

bool Database::CheckDatabaseVersion(DatabaseTypes database)
{
    const DBVersion& core_db_requirements = databaseVersions[database];

    // Fetch the database version table information
    QueryResult* result = Query("SELECT `version`, `structure`, `content`, `description` FROM `db_version` ORDER BY `version` DESC, `structure` DESC, `content` DESC LIMIT 1");

    // db_version table does not exist or is empty
    if (!result)
    {
        sLog.outErrorDb("The table `db_version` in your [%s] database is missing or corrupt.", core_db_requirements.dbname.c_str());
        sLog.outErrorDb();
        sLog.outErrorDb("  [A] You have database Version: MaNGOS can not verify your database version or its existence!");
        sLog.outErrorDb();
        PrintErrorYouNeedDatabaseVersionExpectedByCore(core_db_requirements);
        sLog.outErrorDb();
        sLog.outErrorDb("Please verify your database location or your database integrity.");

        // The core loading will no go further :
        return false;
    }

    Field* fields = result->Fetch();
    std::string current_db_version = fields[0].GetCppString();
    std::string current_db_structure = fields[1].GetCppString();
    std::string current_db_content = fields[2].GetCppString();
    std::string description = fields[3].GetCppString();

    delete result;

    // Structure does not match the required version
    if (current_db_version != core_db_requirements.expected_version || current_db_structure != core_db_requirements.expected_structure)
    {
        sLog.outErrorDb("The table `db_version` indicates that your [%s] database does not match the expected structure!", core_db_requirements.dbname.c_str());
        sLog.outErrorDb();
        PrintErrorYouHaveDatabaseVersion(current_db_version, current_db_structure, current_db_content, description);
        sLog.outErrorDb();
        PrintErrorYouNeedDatabaseVersionExpectedByCore(core_db_requirements);
        sLog.outErrorDb();
        sLog.outErrorDb("You must apply all updates after [A] to [B] to use MaNGOS with this database.");
        sLog.outErrorDb("These updates are included in the database/%s/Updates folder.", core_db_requirements.dbname.c_str());
        return false;
    }

    bool db_vs_core_content_version_mismatch = false;

    // DB is not up to date, but structure is correct.
    // The 'content' version in the 'db_version' table can be < from the one required by the core
    // See  enum values for :
    //  WORLD_DB_CONTENT_NR
    //  CHAR_DB_CONTENT_NR
    //  REALMD_DB_CONTENT_NR
    // for more information.
    if (current_db_content < core_db_requirements.minimal_expected_content)
    {
        // TODO : Should not display with error color but warning (e.g YELLOW) => Create a sLog.outWarningDb() and sLog.outWarning()
        sLog.outErrorDb("You have not updated the core for few DB [%s] updates!", core_db_requirements.dbname.c_str());
        sLog.outErrorDb("Current DB content is %s, core expects %s", current_db_content.c_str(), core_db_requirements.minimal_expected_content.c_str());
        sLog.outErrorDb("It is recommended to run ALL database updates up to the required core version.");
        sLog.outErrorDb("These updates are included in the database/%s/Updates folder.", core_db_requirements.dbname.c_str());
        sLog.outErrorDb("This is ok for now but should not last long.");
        db_vs_core_content_version_mismatch = true;
    }

    // Do not alert if current_db_content > core_db_requirements.minimal_expected_content it can mislead newcomers !

    // In anys cases if there are differences in content : output a recap of the differences :
    if (db_vs_core_content_version_mismatch)
    {
        // TODO : Should not display with error color but warning (e.g YELLOW) => Create a sLog.outWarningDb() and sLog.outWarning()
        sLog.outErrorDb("The table `db_version` indicates that your [%s] database does not match the expected version!", core_db_requirements.dbname.c_str());
        sLog.outErrorDb();
        PrintErrorYouHaveDatabaseVersion(current_db_version, current_db_structure, current_db_content, description);
        sLog.outErrorDb();
        PrintErrorYouNeedDatabaseVersionExpectedByCore(core_db_requirements);
    }
    else
    {
        if (current_db_version == core_db_requirements.expected_version && current_db_structure == core_db_requirements.expected_structure)
        {
            sLog.outString("The table `db_version` indicates that your [%s] database has the same version as the core requirements.", core_db_requirements.dbname.c_str());
            sLog.outString();
        }
        else
        {
            sLog.outString("The table `db_version` indicates that your [%s] database has a higher version than the one referenced by the core."
                "\nYou have probably applied DB updates, and that's a good thing to keep your server up to date.", core_db_requirements.dbname.c_str());
            sLog.outString();
            PrintNormalYouHaveDatabaseVersion(current_db_version, current_db_structure, current_db_content, description);
            sLog.outString();
            PrintNormalDatabaseVersionReferencedByCore(core_db_requirements);
            sLog.outString();
            sLog.outString("You can run the core without any problem like that.");
            sLog.outString();
        }
    }

    return true;
}

bool Database::ExecuteStmt(const SqlStatementID& id, SqlStmtParameters* params)
{
    if (!m_pAsyncConn)
    {
        return false;
    }

    SqlTransaction* pTrans = (*m_TransStorage)->get();
    if (pTrans)
    {
        // add SQL request to trans queue
        pTrans->DelayExecute(new SqlPreparedRequest(id.ID(), params));
    }
    else
    {
        // if async execution is not available
        if (!m_bAllowAsyncTransactions)
        {
            return DirectExecuteStmt(id, params);
        }

        // Simple sql statement
        m_threadBody->Delay(new SqlPreparedRequest(id.ID(), params));
    }

    return true;
}

bool Database::DirectExecuteStmt(const SqlStatementID& id, SqlStmtParameters* params)
{
    MANGOS_ASSERT(params);
    std::shared_ptr<SqlStmtParameters> p(params);
    // execute statement
    SqlConnection::Lock _guard(getAsyncConnection());
    return _guard->ExecuteStmt(id.ID(), *params);
}

SqlStatement Database::CreateStatement(SqlStatementID& index, const char* fmt)
{
    int nId = -1;
    // check if statement ID is initialized
    if (!index.initialized())
    {
        // convert to lower register
        std::string szFmt(fmt);
        // count input parameters
        int nParams = std::count(szFmt.begin(), szFmt.end(), '?');
        // find existing or add a new record in registry
        LOCK_GUARD _guard(m_stmtGuard);
        MANGOS_ASSERT(_guard.locked());
        PreparedStmtRegistry::const_iterator iter = m_stmtRegistry.find(szFmt);
        if (iter == m_stmtRegistry.end())
        {
            nId = ++m_iStmtIndex;
            m_stmtRegistry[szFmt] = nId;
        }
        else
        {
            nId = iter->second;
        }

        // save initialized statement index info
        index.init(nId, nParams);
    }

    return SqlStatement(index, *this);
}

std::string Database::GetStmtString(const int stmtId) const
{
    if (stmtId == -1 || stmtId > m_iStmtIndex)
    {
        return std::string();
    }

    LOCK_GUARD _guard(m_stmtGuard);
    if (_guard.locked())
    {
        PreparedStmtRegistry::const_iterator iter_last = m_stmtRegistry.end();
        for (PreparedStmtRegistry::const_iterator iter = m_stmtRegistry.begin(); iter != iter_last; ++iter)
        {
            if (iter->second == stmtId)
            {
                return iter->first;
            }
        }
    }
    return std::string();
}

// HELPER CLASSES AND FUNCTIONS
Database::TransHelper::~TransHelper()
{
    reset();
}

SqlTransaction* Database::TransHelper::init()
{
    MANGOS_ASSERT(!m_pTrans);   // if we will get a nested transaction request - we MUST fix code!!!
    m_pTrans = new SqlTransaction;
    return m_pTrans;
}

SqlTransaction* Database::TransHelper::detach()
{
    SqlTransaction* pRes = m_pTrans;
    m_pTrans = NULL;
    return pRes;
}

void Database::TransHelper::reset()
{
    delete m_pTrans;
    m_pTrans = NULL;
}
