/**
 * mysqldb.cpp
 * This file is part of the YATE Project http://YATE.null.ro
 *
 * This is the MySQL support from Yate.
 *
 * Yet Another Telephony Engine - a fully featured software PBX and IVR
 * Copyright (C) 2004-2006 Null Team
 *
 * 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include <yatephone.h>

#include <stdio.h>
#include <mysql.h>

#ifndef CLIENT_MULTI_STATEMENTS
#define CLIENT_MULTI_STATEMENTS 0
#define mysql_next_result(x) (-1)
#endif

#if (MYSQL_VERSION_ID > 41000)
#define HAVE_MYSQL_410
#else
#define mysql_warning_count(x) (0)
#define mysql_library_init mysql_server_init
#define mysql_library_end mysql_server_end
#endif

using namespace TelEngine;
namespace { // anonymous

static ObjList s_conns;
Mutex s_conmutex;

class DbConn : public GenObject
{
public:
    DbConn(const NamedList* sect);
    ~DbConn();
    virtual const String& toString() const
	{ return m_name; }

    bool ok();
    int queryDb(const char* query, Message* dest = 0);
    bool initDb();
    void dropDb();
    inline Mutex& mutex()
	{ return m_dbmutex; }
    void runQueries();

protected:
    Mutex m_dbmutex;

private:
    bool testDb();
    bool startDb();
    int queryDbInternal();
    String m_name;
    unsigned int m_timeout;
    MYSQL *m_conn;
    String m_host;
    String m_user;
    String m_pass;
    String m_db;
    String m_unix;
    unsigned int m_port;
    bool m_compress;
    String m_query;
    Message* m_msg;
    int m_res;
    volatile bool m_go;
};

class DbThread : public Thread
{
public:
    inline DbThread(DbConn* conn)
	: Thread("mysqldb"), m_conn(conn)
	{ }
    virtual void run();
    virtual void cleanup();
private:
    DbConn* m_conn;
};

class MyHandler : public MessageHandler
{
public:
    MyHandler(unsigned int prio = 100)
	: MessageHandler("database",prio)
	{ }
    virtual bool received(Message& msg);
};

class MyModule : public Module
{
public:
    MyModule();
    ~MyModule();
protected:
    virtual void initialize();
    virtual void statusParams(String& str);
private:
    bool m_init;
};

static MyModule module;

DbConn::DbConn(const NamedList* sect)
    : m_dbmutex(true), m_name(*sect), m_conn(0), m_msg(0), m_go(false)
{
    int tout = sect->getIntValue("timeout",10000);
    // round to seconds
    m_timeout = (tout + 500) / 1000;
    // but make sure it doesn't round to zero unless zero was requested
    if (tout && !m_timeout)
	m_timeout = 1;
    m_host = sect->getValue("host");
    m_user = sect->getValue("user","mysql");
    m_pass = sect->getValue("password");
    m_db = sect->getValue("database","yate");
    m_port = sect->getIntValue("port");
    m_unix = sect->getValue("socket");
    m_compress = sect->getBoolValue("compress");
}

DbConn::~DbConn()
{ 
    s_conns.remove(this,false);
    // FIXME: should we try to do it from this thread?
    dropDb();
}

// initialize the database connection
bool DbConn::initDb()
{
    Lock lock(m_dbmutex);
    // allow specifying the raw connection string
    Debug(&module,DebugInfo,"Initiating connection for '%s'",m_name.c_str());
    m_conn = mysql_init(m_conn);
    if (!m_conn) {
	Debug(&module,DebugGoOn,"Could not start connection for '%s'",m_name.c_str());
	return false;
    }
    if (m_compress)
	mysql_options(m_conn,MYSQL_OPT_COMPRESS,0);
    mysql_options(m_conn,MYSQL_OPT_CONNECT_TIMEOUT,(const char*)&m_timeout);
#ifdef MYSQL_OPT_READ_TIMEOUT
    mysql_options(m_conn,MYSQL_OPT_READ_TIMEOUT,(const char*)&m_timeout);
#endif
#ifdef MYSQL_OPT_WRITE_TIMEOUT
    mysql_options(m_conn,MYSQL_OPT_WRITE_TIMEOUT,(const char*)&m_timeout);
#endif
    if (mysql_real_connect(m_conn,m_host,m_user,m_pass,m_db,m_port,m_unix,CLIENT_MULTI_STATEMENTS)) {
#ifdef MYSQL_OPT_RECONNECT
	// this option must be set after connect - bug in mysql client library
	my_bool reconn = 1;
	mysql_options(m_conn,MYSQL_OPT_RECONNECT,(const char*)&reconn);
#endif
	return true;
    }
    Debug(&module,DebugWarn,"Connection for '%s' failed: %s",m_name.c_str(),mysql_error(m_conn));
    return false;
}

// drop the connection
void DbConn::dropDb()
{
    Lock lock(m_dbmutex);
    m_res = -1;
    m_go = false;
    if (!m_conn)
	return;
    MYSQL* tmp = m_conn;
    m_conn = 0;
    lock.drop();
    mysql_close(tmp);
    Debug(&module,DebugInfo,"Database connection '%s' closed",m_name.c_str());
}

// test it the connection is still OK
bool DbConn::testDb()
{
    return m_conn && !mysql_ping(m_conn);
}

// perform the query, fill the message with data
//  return number of rows, -1 for error
int DbConn::queryDbInternal()
{
    if (!testDb())
	return -1;

    if (mysql_query(m_conn,m_query)) {
	Debug(&module,DebugWarn,"Query for '%s' failed: %s",m_name.c_str(),mysql_error(m_conn));
	return -1;
    }

    int total = 0;
    unsigned int warns = 0;
    unsigned int affected = 0;
    do {
	MYSQL_RES* res = mysql_store_result(m_conn);
	warns += mysql_warning_count(m_conn);
	affected += mysql_affected_rows(m_conn);
	if (res) {
	    unsigned int cols = mysql_num_fields(res);
	    unsigned int rows = mysql_num_rows(res);
	    Debug(&module,DebugAll,"Got result set %p rows=%u cols=%u",res,rows,cols);
	    total += rows;
	    if (m_msg) {
		MYSQL_FIELD* fields = mysql_fetch_fields(res);
		m_msg->setParam("columns",String(cols));
		m_msg->setParam("rows",String(rows));
		if (m_msg->getBoolValue("results",true)) {
		    Array *a = new Array(cols,rows+1);
		    unsigned int c;
		    // add field names
		    for (c = 0; c < cols; c++)
			a->set(new String(fields[c].name),c,0);
		    // and now data row by row
		    for (unsigned int r = 1; r <= rows; r++) {
			MYSQL_ROW row = mysql_fetch_row(res);
			if (!row)
			    break;
			for (c = 0; c < cols; c++) {
			    if (row[c])
				a->set(new String(row[c]),c,r);
			}
		    }
		    m_msg->userData(a);
		    a->deref();
		}
	    }
	    mysql_free_result(res);
	}
    } while (!mysql_next_result(m_conn));

    if (m_msg) {
	m_msg->setParam("affected",String(affected));
	if (warns)
	    m_msg->setParam("warnings",String(warns));
    }
    return total;
}

void DbConn::runQueries()
{
    while (m_conn) {
	if (m_go) {
	    DDebug(&module,DebugAll,"Running query \"%s\" for '%s'",
		m_query.c_str(),m_name.c_str());
	    m_res = queryDbInternal();
	    m_msg = 0;
	    m_query.clear();
	    m_go = false;
	}
	Thread::yield(true);
    }
}

static bool failure(Message* m)
{
    if (m)
	m->setParam("error","failure");
    return false;
}

int DbConn::queryDb(const char* query, Message* dest)
{
    if (TelEngine::null(query))
	return -1;
    DDebug(&module,DebugAll,"Proxying query \"%s\" for '%s'",
	query,m_name.c_str());
    m_msg = dest;
    m_query = query;
    m_go = true;
    while (m_go)
	Thread::yield();
    if (m_res < 0)
	failure(dest);
    return m_res;
}


void DbThread::run()
{
    mysql_library_init(0,0,0);
    if (m_conn->initDb())
	m_conn->runQueries();
}

void DbThread::cleanup()
{
    m_conn->dropDb();
    mysql_library_end();
    mysql_thread_end();
}


static DbConn* findDb(String& account)
{
    if (account.null())
	return 0;
    ObjList* l = s_conns.find(account);
    return l ? static_cast<DbConn *>(l->get()): 0;
}

bool MyHandler::received(Message& msg)
{
    String tmp(msg.getValue("account"));
    if (tmp.null())
	return false;
    Lock lock(s_conmutex);
    DbConn* db = findDb(tmp);
    if (!db)
	return false;
    Lock lo(db->mutex());
    lock.drop();
    String query(msg.getValue("query"));
    db->queryDb(query,&msg);
    msg.setParam("dbtype","mysqldb");
    return true;
}

MyModule::MyModule()
    : Module ("mysqldb","database"),m_init(false)
{
    Debug(DebugAll,"Loaded module MySQL based on %s",mysql_get_client_info());
}

MyModule::~MyModule()
{
    s_conns.clear();
    Debug(DebugAll,"Unloaded module MySQL");
}

void MyModule::statusParams(String& str)
{
    str.append("conns=",",") << s_conns.count();
}

void MyModule::initialize()
{
    Module::initialize();
    if (m_init)
	return;
    m_init = true;
    Debug(DebugAll,"Initializing module MySQL");
    Configuration cfg(Engine::configFile("mysqldb"));
    Engine::install(new MyHandler(cfg.getIntValue("general","priority",50)));
    unsigned int i;
    for (i = 0; i < cfg.sections(); i++) {
	NamedList* sec = cfg.getSection(i);
	if (!sec || (*sec == "general"))
	    continue;
	DbConn* conn = new DbConn(sec);
	DbThread* thr = new DbThread(conn);
	if (thr->startup())
	    s_conns.insert(conn);
	else {
	    delete thr;
	    conn->destruct();
	}
    }
}

}; // anonymous namespace

/* vi: set ts=8 sw=4 sts=4 noet: */
