// This file is part of BOINC.
// http://boinc.berkeley.edu
// Copyright (C) 2008 University of California
//
// BOINC is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License
// as published by the Free Software Foundation,
// either version 3 of the License, or (at your option) any later version.
//
// BOINC 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 Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with BOINC.  If not, see <http://www.gnu.org/licenses/>.

#include "config.h"
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <mysql.h>

#include "error_numbers.h"
#include "str_util.h"
#include "str_replace.h"
#include "db_base.h"

#ifdef _USING_FCGI_
#include "fcgi_stdio.h"
#endif

// uncomment the following to print all queries.
// Useful for low-level debugging

//#define SHOW_QUERIES

DB_CONN::DB_CONN() {
    mysql = 0;
}

int DB_CONN::open(char* db_name, char* db_host, char* db_user, char* dbpassword) {
    mysql = mysql_init(0);
    if (!mysql) return ERR_DB_CANT_INIT;
#if MYSQL_VERSION_ID >= 50106
    my_bool mbReconnect = 1;
    mysql_options(mysql, MYSQL_OPT_RECONNECT, &mbReconnect);
#endif
    mysql = mysql_real_connect(mysql, db_host, db_user, dbpassword, db_name, 0, 0, 0);
    if (mysql == 0) return ERR_DB_CANT_CONNECT;

    // older versions of MySQL lib need to set the option AFTER connecting;
    // see http://dev.mysql.com/doc/refman/5.1/en/mysql-options.html
    //
#if MYSQL_VERSION_ID >= 50013 && MYSQL_VERSION_ID < 50106
    my_bool mbReconnect = 1;
    mysql_options(mysql, MYSQL_OPT_RECONNECT, &mbReconnect);
#endif
    return 0;
}

void DB_CONN::close() {
    if (mysql) mysql_close(mysql);
}

int DB_CONN::set_isolation_level(ISOLATION_LEVEL level) {
    const char* level_str;
    char query[256];

    switch(level) {
    case READ_UNCOMMITTED:
        level_str = "READ UNCOMMITTED";
        break;
    case READ_COMMITTED:
        level_str = "READ COMMITTED";
        break;
    case REPEATABLE_READ:
        level_str = "REPEATABLE READ";
        break;
    case SERIALIZABLE:
        level_str = "SERIALIZABLE";
        break;
    default:
        return -1;
    }
    sprintf(query, "SET SESSION TRANSACTION ISOLATION LEVEL %s", level_str);
    return do_query(query);
}

int DB_CONN::do_query(const char* p) {
    int retval;
#ifdef SHOW_QUERIES
    fprintf(stderr, "query: %s\n", p);
#endif
    retval = mysql_query(mysql, p);
    if (retval) {
        fprintf(stderr, "Database error: %s\nquery=%s\n", error_string(), p);
    }
    return retval;
}

int DB_CONN::affected_rows() {
    unsigned long x = (unsigned long)mysql_affected_rows(mysql);
    //fprintf(stderr, "x: %lu i: %d\n", x, (int)x);
    return (int)x;
}

int DB_CONN::insert_id() {
    int retval;
    MYSQL_ROW row;
    MYSQL_RES* rp;

    retval = do_query("select LAST_INSERT_ID()");
    if (retval) return retval;
    rp = mysql_store_result(mysql);
    row = mysql_fetch_row(rp);
    int x = atoi(row[0]);
    mysql_free_result(rp);
    return x;
}

void DB_CONN::print_error(const char* p) {
    fprintf(stderr, "%s: Database error: %s\n", p, error_string());
}

const char* DB_CONN::error_string() {
    return mysql?mysql_error(mysql):"Not connected";
}

int DB_CONN::start_transaction() {
    return do_query("START TRANSACTION");
}

int DB_CONN::commit_transaction() {
    return do_query("COMMIT");
}

int DB_CONN::rollback_transaction() {
	return do_query("ROLLBACK");
}

int DB_CONN::ping() {
    int retval = mysql_ping(mysql);
    if (retval) return ERR_DB_CANT_CONNECT;
    return 0;
}

DB_BASE::DB_BASE(const char *tn, DB_CONN* p) : db(p), table_name(tn) {
}

int DB_BASE::get_id() { return 0;}
void DB_BASE::db_print(char*) {}

void DB_BASE::db_parse(MYSQL_ROW&) {}

int DB_BASE::insert() {
    char vals[MAX_QUERY_LEN], query[MAX_QUERY_LEN];
    db_print(vals);
    sprintf(query, "insert into %s set %s", table_name, vals);
    return db->do_query(query);
}

int DB_BASE::insert_batch(std::string& values) {
    std::string query;
    query = "insert into " + std::string(table_name) + " values " + values;
    return db->do_query(query.c_str());
}

int DB_BASE::affected_rows() {
    return db->affected_rows();
}

// update an entire record
//
int DB_BASE::update() {
    char vals[MAX_QUERY_LEN], query[MAX_QUERY_LEN];
    db_print(vals);
    sprintf(query, "update %s set %s where id=%d", table_name, vals, get_id());
    int retval = db->do_query(query);
    if (retval) return retval;
    if (db->affected_rows() != 1) return ERR_DB_NOT_FOUND;
    return 0;
}

// update one or more fields
// "clause" is something like "foo=5, blah='xxx'" or "foo=foo+5"
//
int DB_BASE::update_field(const char* clause, const char* where_clause) {
    char query[MAX_QUERY_LEN];
    if (where_clause) {
        sprintf(query, "update %s set %s where id=%d and %s", table_name, clause, get_id(), where_clause);
    } else {
        sprintf(query, "update %s set %s where id=%d", table_name, clause, get_id());
    }
    return db->do_query(query);
}

// delete record
//
int DB_BASE::delete_from_db() {
    char vals[MAX_QUERY_LEN], query[MAX_QUERY_LEN];
    db_print(vals);
    sprintf(query, "delete from %s where id=%d", table_name, get_id());
    return db->do_query(query);
}

int DB_BASE::get_field_int(const char* field, int& val) {
    char query[MAX_QUERY_LEN];
    int retval;
    MYSQL_ROW row;
    MYSQL_RES* rp;

    sprintf(query, "select %s from %s where id=%d", field, table_name, get_id());
    retval = db->do_query(query);
    if (retval) return retval;
    rp = mysql_store_result(db->mysql);
    if (!rp) return -1;
    row = mysql_fetch_row(rp);
    if (row) val = atoi(row[0]);
    mysql_free_result(rp);
    if (row == 0) return ERR_DB_NOT_FOUND;
    return 0;
}

int DB_BASE::get_field_str(const char* field, char* buf, int buflen) {
    char query[MAX_QUERY_LEN];
    int retval;
    MYSQL_ROW row;
    MYSQL_RES* rp;

    sprintf(query, "select %s from %s where id=%d", field, table_name, get_id());
    retval = db->do_query(query);
    if (retval) return retval;
    rp = mysql_store_result(db->mysql);
    if (!rp) return -1;
    row = mysql_fetch_row(rp);
    if (row && row[0]) {
        strlcpy(buf, row[0], buflen);
    }
    mysql_free_result(rp);
    if (row == 0) return ERR_DB_NOT_FOUND;
    return 0;
}

int DB_BASE::lookup(const char* clause) {
    char query[MAX_QUERY_LEN];
    int retval;
    MYSQL_ROW row;
    MYSQL_RES* rp;

    sprintf(query, "select * from %s %s", table_name, clause);

    retval = db->do_query(query);
    if (retval) return retval;
    rp = mysql_store_result(db->mysql);
    if (!rp) return -1;
    row = mysql_fetch_row(rp);
    if (row) db_parse(row);
    mysql_free_result(rp);
    if (row == 0) return ERR_DB_NOT_FOUND;
    return 0;
}

int DB_BASE::lookup_id(int id) {
    char query[MAX_QUERY_LEN];
    int retval;
    MYSQL_ROW row;
    MYSQL_RES* rp;

    sprintf(query, "select * from %s where id=%d", table_name, id);

    retval = db->do_query(query);
    if (retval) return retval;
    rp = mysql_store_result(db->mysql);
    if (!rp) return -1;
    row = mysql_fetch_row(rp);
    if (row) db_parse(row);
    mysql_free_result(rp);
    if (row == 0) return ERR_DB_NOT_FOUND;

    // don't bother checking for uniqueness here
    return 0;
}

int DB_BASE::enumerate(const char* clause, bool use_use_result) {
    int x;
    char query[MAX_QUERY_LEN];
    MYSQL_ROW row;

    if (!cursor.active) {
        cursor.active = true;

        sprintf(query, "select * from %s %s", table_name, clause);

        x = db->do_query(query);
        if (x) return mysql_errno(db->mysql);

        // if you use mysql_use_result() here,
        // any other transactions will fail
        //
        if (use_use_result) {
            cursor.rp = mysql_use_result(db->mysql);
        } else {
            cursor.rp = mysql_store_result(db->mysql);
        }
        if (!cursor.rp) return mysql_errno(db->mysql);
    }
    row = mysql_fetch_row(cursor.rp);
    if (!row) {
        mysql_free_result(cursor.rp);
        cursor.active = false;
        x = mysql_errno(db->mysql);
        if (x) return x;
        return ERR_DB_NOT_FOUND;
    } else {
        db_parse(row);
    }
    return 0;
}

// call this to end an enumeration before reaching end
//
int DB_BASE::end_enumerate() {
    if (cursor.active) {
        mysql_free_result(cursor.rp);
        cursor.active = false;
    }
    return 0;
}

int DB_BASE::get_integer(const char* query, int& n) {
    int retval;
    MYSQL_ROW row;
    MYSQL_RES* resp;

    retval = db->do_query(query);
    if (retval) return retval;
    resp = mysql_store_result(db->mysql);
    if (!resp) return ERR_DB_NOT_FOUND;
    row = mysql_fetch_row(resp);
    if (!row || !row[0]) {
        retval = ERR_DB_NOT_FOUND;
    } else {
        n = atoi(row[0]);
    }
    mysql_free_result(resp);
    return retval;
}

int DB_BASE::get_double(const char* query, double& x) {
    int retval;
    MYSQL_ROW row;
    MYSQL_RES* resp;

    retval = db->do_query(query);
    if (retval) return retval;
    resp = mysql_store_result(db->mysql);
    if (!resp) return ERR_DB_NOT_FOUND;
    row = mysql_fetch_row(resp);
    if (!row || !row[0]) {
        retval = ERR_DB_NOT_FOUND;
    } else {
        x = atof(row[0]);
    }
    mysql_free_result(resp);
    return retval;
}

int DB_BASE::count(int& n, const char* clause) {
    char query[MAX_QUERY_LEN];

    sprintf(query, "select count(*) from %s %s", table_name, clause);

    return get_integer(query, n);
}

int DB_BASE::max_id(int& n, const char* clause) {
    char query[MAX_QUERY_LEN];
    sprintf(query, "select max(id) from %s %s", table_name, clause);
    return get_integer(query, n);
}

int DB_BASE::sum(double& x, const char* field, const char* clause) {
    char query[MAX_QUERY_LEN];

    sprintf(query, "select sum(%s) from %s %s", field, table_name, clause);

    return get_double(query, x);
}

DB_BASE_SPECIAL::DB_BASE_SPECIAL(DB_CONN* p) : db(p) {
}

// convert a string into a form that allows it to be used
// in SQL queries delimited by single quotes:
// replace ' with \', '\' with '\\'
//
void escape_string(char* field, int len) {
    char buf[MAX_QUERY_LEN];
    char* q = buf, *p = field;

    // Make sure that final result won't overflow field[].
    // Don't need to worry about overflowing buf[] since 
    // in worst case string length only doubles.
    //
    while (*p && q < buf+len-2) {
        if (*p == '\'') {
            // this does ' to \' transformation 
            //
            *q++ = '\\';
            *q++ = '\'';
        } else if (*p == '\\') {
            // this does \ to \\ transformation
            //
            *q++ = '\\';
            *q++ = '\\';
        } else {
            // this handles all other characters
            //
            *q++ = *p;
        }
        p++;
    }
    *q = 0;
    strlcpy(field, buf, len);
}

// undo the above process
// (len not used because this doesn't expand the string)
//
void unescape_string(char* p, int /*len*/) {
    char* q = p;
    while (*p) {
        if (*p == '\\') {
            p++;
            if (!*p) break; // SHOULD NEVER HAPPEN!
        }
        *q++ = *p++;
    }
    *q='\0';
}

// replace _ with \\_, % with \\%
//
void escape_mysql_like_pattern(const char* in, char* out) {
    while (*in) {
        if (*in == '_' || *in == '%') {
            *out++ = '\\';
            *out++ = '\\';
        }
        *out++ = *in++;
    }
}

const char *BOINC_RCSID_43d919556b = "$Id: db_base.cpp 18437 2009-06-16 20:54:44Z davea $";
