// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.

#include <iostream>
#include <sstream>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include <mysql.h>

#include "benchmark.h"
#include "mysql_ubench.h"

using namespace std;

MySQL_uBenchmark::MySQL_uBenchmark(const string& hostname, const string& user,
                                   const string& pass, const string& db,
                                   uint32_t num_iterations, bool sync,
                                   bool verbose)
    :uBenchmark(num_iterations, db, sync, verbose, hostname, user, pass),
     conn_(NULL) {

}

void MySQL_uBenchmark::stmt_failure(MYSQL_STMT * stmt, const char* operation) {
    stringstream tmp;
    tmp << "Error " << mysql_stmt_errno(stmt) << " during " << operation
        << ": " << mysql_stmt_error(stmt);
    throw tmp.str();
}



void MySQL_uBenchmark::failure(const char* operation) {
    stringstream tmp;
    tmp << "Error " << mysql_errno(conn_) << " during " << operation
        << ": " << mysql_error(conn_);
    throw tmp.str();
}

void MySQL_uBenchmark::connect() {

    conn_ = mysql_init(NULL);
    if (!conn_) {
        failure("initializing MySQL library");
    } else {
        cout << "MySQL library init successful." << endl;
    }

    if (!mysql_real_connect(conn_, hostname_.c_str(), user_.c_str(),
                            passwd_.c_str(), dbname_.c_str(), 0, NULL, 0)) {
        failure("connecting to MySQL server");
    } else {
        cout << "MySQL connection established." << endl;
    }

    string q = "delete from lease4;";
    if (mysql_real_query(conn_, q.c_str(), strlen(q.c_str()))) {
        failure("dropping old lease4 entries.");
    }

    q = "ALTER TABLE lease4 engine=";
    if (sync_) {
        q += "InnoDB";
    } else {
        q += "MyISAM";
    }
    if (mysql_query(conn_, q.c_str())) {
        q = "Failed to run query:" + q;
        failure(q.c_str());
    }
}

void MySQL_uBenchmark::disconnect() {
    if (!conn_) {
        throw "NULL MySQL connection pointer.";
    }
    mysql_close(conn_);
    conn_ = NULL;
}

void MySQL_uBenchmark::createLease4Test() {
    if (!conn_) {
        throw "Not connected to MySQL server.";
    }

    uint32_t addr = BASE_ADDR4; // Let's start with 1.0.0.0 address
    char hwaddr[20];
    size_t hwaddr_len = 20;    // Not a real field
    char client_id[128];
    size_t client_id_len = 128;
    uint32_t valid_lft = 1000;  // We can use the same value for all leases
    uint32_t recycle_time = 7;  //    not supported in any foresable future,

    char cltt[48];              // timestamp (specified as text)
    size_t cltt_len;

    sprintf(cltt, "2012-07-11 15:43:00");
    cltt_len = strlen(cltt);

    uint32_t pool_id = 1000;       // Let's use pool-ids greater than zero
    bool fixed = false;

    char hostname[] = "foo";    // Will generate it dynamically
    size_t hostname_len;
    hostname_len = strlen(hostname);

    bool fqdn_fwd = true;       // Let's pretend to do AAAA update
    bool fqdn_rev = true;       // Let's pretend to do PTR update

    cout << "CREATE:   ";

    for (uint8_t i = 0; i < hwaddr_len; i++) {
        hwaddr[i] = 'A' + i; // let's make hwaddr consisting of letters
    }
    hwaddr[19] = 0; // make it is null-terminated

    for (uint8_t i = 0; i < client_id_len; i++) {
        client_id[i] = 33 + i; // 33 is being the first, non whitespace
                               // printable ASCII character
    }
    client_id[127] = 0; // make it is null-terminated

    MYSQL_STMT * stmt = NULL;
    MYSQL_BIND bind[11]; // 11 parameters in the insert statement

    if (compiled_stmt_) {
        // create a statement once
        stmt = mysql_stmt_init(conn_);
        if (!stmt) {
            failure("Unable to create compiled statement, mysql_stmt_init() failed");
        }

        const char * statement = "INSERT INTO lease4(addr,hwaddr,client_id,"
            "valid_lft,recycle_time,cltt,pool_id,fixed,hostname,"
            "fqdn_fwd,fqdn_rev) VALUES(?,?,?,?,?,?,?,?,?,?,?)";

        if (mysql_stmt_prepare(stmt, statement, strlen(statement) )) {
            failure("Failed to prepare statement, mysql_stmt_prepare() returned non-zero");
        }
        int param_cnt = mysql_stmt_param_count(stmt);
        if (param_cnt != 11) {
            failure("Parameter count sanity check failed.");
        }

        memset(bind, 0, sizeof(bind));

        // 1st parameter: IPv4 address
        bind[0].buffer_type = MYSQL_TYPE_LONG;
        bind[0].buffer = (&addr);
        bind[0].is_null = 0;
        bind[0].length = 0;

        // 2nd parameter: Hardware address
        bind[1].buffer_type = MYSQL_TYPE_VARCHAR;
        bind[1].buffer = hwaddr;
        bind[1].buffer_length = hwaddr_len;
        bind[1].is_null = 0;
        bind[1].length = &hwaddr_len;

        // 3rd parameter: Client-id
        bind[2].buffer_type = MYSQL_TYPE_VARCHAR;
        bind[2].buffer = client_id;
        bind[2].buffer_length = client_id_len;
        bind[2].is_null = 0;
        bind[2].length = &client_id_len;

        // 4th parameter: valid-lifetime
        bind[3].buffer_type = MYSQL_TYPE_LONG;
        bind[3].buffer = (&valid_lft);
        bind[3].is_null = 0;
        bind[3].length = 0;

        // 5th parameter: recycle-time
        bind[4].buffer_type = MYSQL_TYPE_LONG;
        bind[4].buffer = (&recycle_time);
        bind[4].is_null = 0;
        bind[4].length = 0;

        // 6th parameter: cltt
        bind[5].buffer_type = MYSQL_TYPE_TIMESTAMP;
        bind[5].buffer = cltt;
        bind[2].buffer_length = cltt_len;
        bind[5].is_null = 0;
        bind[5].length = &cltt_len;

        // 7th parameter: pool-id
        bind[6].buffer_type = MYSQL_TYPE_LONG;
        bind[6].buffer = &pool_id;
        bind[6].is_null = 0;
        bind[6].length = 0;

        // 8th parameter: fixed
        bind[7].buffer_type = MYSQL_TYPE_TINY;
        bind[7].buffer = &fixed;
        bind[7].is_null = 0;
        bind[7].length = 0;

        // 9th parameter: hostname
        bind[8].buffer_type = MYSQL_TYPE_VARCHAR;
        bind[8].buffer = hostname;
        bind[8].buffer_length = strlen(hostname);
        bind[8].is_null = 0;
        bind[8].length = &hostname_len;

        // 10th parameter: fqdn_fwd
        bind[9].buffer_type = MYSQL_TYPE_TINY;
        bind[9].buffer = &fqdn_fwd;
        bind[9].is_null = 0;
        bind[9].length = 0;

        // 11th parameter: fqdn_rev
        bind[10].buffer_type = MYSQL_TYPE_TINY;
        bind[10].buffer = &fqdn_rev;
        bind[10].is_null = 0;
        bind[10].length = 0;
    }

    for (uint32_t i = 0; i < num_; i++) {

        sprintf(cltt, "2012-07-11 15:43:%02d", i % 60);


        addr++;

        if (!compiled_stmt_) {
            // the first address is 1.0.0.0.
            char query[2000], * end;
            strcpy(query, "INSERT INTO lease4(addr,hwaddr,client_id,"
                   "valid_lft,recycle_time,cltt,pool_id,fixed,hostname,"
                   "fqdn_fwd,fqdn_rev) VALUES(");
            end = query + strlen(query);
            end += sprintf(end, "%u,\'", addr);
            end += mysql_real_escape_string(conn_, end, hwaddr, hwaddr_len);
            end += sprintf(end,"\',\'");
            end += mysql_real_escape_string(conn_, end, client_id, client_id_len);
            end += sprintf(end, "\',%d,%d,'%s',%d,%s,\'%s\',%s,%s);",
                           valid_lft, recycle_time, cltt,
                           pool_id, (fixed?"true":"false"), hostname,
                           (fqdn_fwd?"true":"false"), (fqdn_rev?"true":"false"));
            // lease_id field is set automatically
            // options and comments fields are not set

            unsigned int len = end - query;
            if (mysql_real_query(conn_, query, len)) {
                // something failed.
                failure("INSERT query");
            }
        } else {
            // compiled statement

            if (mysql_stmt_bind_param(stmt, bind)) {
                failure("Failed to bind parameters: mysql_stmt_bind_param() returned non-zero");
            }

            if (mysql_stmt_execute(stmt)) {
                failure("Failed to execute statement: mysql_stmt_execute() returned non-zero");
            }

        }

        if (verbose_) {
            cout << ".";
        }
    }

    if (compiled_stmt_) {
        if (mysql_stmt_close(stmt)) {
            failure("Failed to close compiled statement, mysql_stmt_close returned non-zero");
        }
    }

    cout << endl;
}

void MySQL_uBenchmark::searchLease4Test() {
    if (!conn_) {
        throw "Not connected to MySQL server.";
    }

    cout << "RETRIEVE: ";

    uint32_t addr = 0;

    MYSQL_STMT * stmt = NULL;
    MYSQL_BIND bind[1]; // just a single element
    if (compiled_stmt_) {
        stmt = mysql_stmt_init(conn_);
        if (!stmt) {
            failure("Unable to create compiled statement");
        }
        const char * statement = "SELECT lease_id,addr,hwaddr,client_id,"
            "valid_lft, cltt,pool_id,fixed,hostname,fqdn_fwd,fqdn_rev "
            "FROM lease4 where addr=?";
        if (mysql_stmt_prepare(stmt, statement, strlen(statement))) {
           failure("Failed to prepare statement, mysql_stmt_prepare() returned non-zero");
        }
        int param_cnt = mysql_stmt_param_count(stmt);
        if (param_cnt != 1) {
            failure("Parameter count sanity check failed.");
        }

        memset(bind, 0, sizeof(bind));

        // 1st parameter: IPv4 address
        bind[0].buffer_type = MYSQL_TYPE_LONG;
        bind[0].buffer = (&addr);
        bind[0].is_null = 0;
        bind[0].length = 0;
    }

    for (uint32_t i = 0; i < num_; i++) {

        addr = BASE_ADDR4 + random() % int(num_ / hitratio_);

        if (!compiled_stmt_) {
            char query[512];
            sprintf(query, "SELECT lease_id,addr,hwaddr,client_id,valid_lft,"
                    "cltt,pool_id,fixed,hostname,fqdn_fwd,fqdn_rev "
                    "FROM lease4 where addr=%d", addr);
            mysql_real_query(conn_, query, strlen(query));

            MYSQL_RES * result = mysql_store_result(conn_);

            int num_rows = mysql_num_rows(result);
            int num_fields = mysql_num_fields(result);

            if ( (num_rows > 1) ) {
                stringstream tmp;
                tmp << "Search: DB returned " << num_rows << " leases for address "
                    << hex << addr << dec;
                failure(tmp.str().c_str());
            }

            if (num_rows) {
                if (num_fields == 0) {
                    failure("Query returned empty set");
                }

                MYSQL_ROW row = mysql_fetch_row(result);

                // pretend to do something with it
                if (row[0] == NULL) {
                    failure("SELECT returned NULL data.");
                }
                mysql_free_result(result);

                if (verbose_) {
                    cout << "."; // hit
                }

            } else {
                if (verbose_) {
                    cout << "x"; // miss
                }
            }
        } else {
            // compiled statement

            if (mysql_stmt_bind_param(stmt, bind)) {
                failure("Failed to bind parameters: mysql_stmt_bind_param() returned non-zero");
            }

            if (mysql_stmt_execute(stmt)) {
                failure("Failed to execute statement: mysql_stmt_execute() returned non-zero");
            }

            MYSQL_BIND response[11];

            size_t length[11];
            my_bool is_null[11];
            my_bool error[11];

            uint32_t lease_id;
            uint32_t lease_addr;
            char hwaddr[20];
            char client_id[128];
            uint32_t valid_lft;  // We can use the same value for all leases
            MYSQL_TIME cltt;
            uint32_t pool_id;
            my_bool fixed;
            char hostname[255];
            my_bool fqdn_fwd;
            my_bool fqdn_rev;

            for (int j = 0; j < 11; j++) {
                response[j].is_null = &is_null[j];
                response[j].length = &length[j];
                response[j].error = &error[j];
            }

            // 1th parameter: lease_id
            response[0].buffer_type = MYSQL_TYPE_LONG;
            response[0].buffer = (&lease_id);

            // 2nd parameter: IPv4 address
            response[1].buffer_type = MYSQL_TYPE_LONG;
            response[1].buffer = (&lease_addr);

            // 3rd parameter: Hardware address
            response[2].buffer_type = MYSQL_TYPE_STRING;
            response[2].buffer = hwaddr;
            response[2].buffer_length = sizeof(hwaddr);

            // 4th parameter: Client-id
            response[3].buffer_type = MYSQL_TYPE_STRING;
            response[3].buffer = &client_id;
            response[3].buffer_length = sizeof(client_id);

            // 5th parameter: valid-lifetime
            response[4].buffer_type = MYSQL_TYPE_LONG;
            response[4].buffer = &valid_lft;

            // 6th parameter: cltt
            response[5].buffer_type = MYSQL_TYPE_TIMESTAMP;
            response[5].buffer = &cltt;

            // 7th parameter: pool-id
            response[6].buffer_type = MYSQL_TYPE_LONG;
            response[6].buffer = &pool_id;

            // 8th parameter: fixed
            response[7].buffer_type = MYSQL_TYPE_TINY;
            response[7].buffer = &fixed;

            // 9th parameter: hostname
            response[8].buffer_type = MYSQL_TYPE_STRING;
            response[8].buffer = &hostname;

            // 10th parameter: fqdn_fwd
            response[9].buffer_type = MYSQL_TYPE_TINY;
            response[9].buffer = &fqdn_fwd;

            // 11th parameter: fqdn_rev
            response[10].buffer_type = MYSQL_TYPE_TINY;
            response[10].buffer = &fqdn_rev;

            if (mysql_stmt_bind_result(stmt, response))
            {
                cout << "Error:" << mysql_stmt_error(stmt) << endl;
                failure("mysql_stmt_bind_result() failed");
            }
            int num_rows = 0;

            int result = mysql_stmt_fetch(stmt);
            switch (result) {
            case 0: {
                if (lease_addr != addr) {
                    failure("Returned data is bogus!");
                }
                num_rows++;
                break;
            }
            case MYSQL_NO_DATA:
            {
                // that's ok. We randomized non-existing address
                break;

            }
            default: {
                stmt_failure(stmt, "RETRIEVE (mysql_stmt_fetch())");
            }
            }

            // we could call mysql_stmt_fetch again to check that there are no
            // other data for us. But there should be exactly one row of data
            // with specified address.

            if (num_rows) {
                if (verbose_) {
                    cout << "."; // hit
                }
            } else {
                if (verbose_) {
                    cout << "X"; // miss
                }
            }

        }
    }

    if (compiled_stmt_) {
        if (mysql_stmt_close(stmt)) {
            failure("Failed to close compiled statement, mysql_stmt_close returned non-zero");
        }
    }

    cout << endl;
}

void MySQL_uBenchmark::updateLease4Test() {
    if (!conn_) {
        throw "Not connected to MySQL server.";
    }

    cout << "UPDATE:   ";

    uint32_t valid_lft = 1002; // just some dummy value
    char cltt[] = "now()";
    size_t cltt_len = strlen(cltt);
    uint32_t addr = 0;

    MYSQL_STMT * stmt = NULL;
    MYSQL_BIND bind[3];
    if (compiled_stmt_) {
        stmt = mysql_stmt_init(conn_);
        if (!stmt) {
            failure("Unable to create compiled statement");
        }
        const char * statement = "UPDATE lease4 SET valid_lft=?, cltt=? WHERE addr=?";
        if (mysql_stmt_prepare(stmt, statement, strlen(statement))) {
           failure("Failed to prepare statement, mysql_stmt_prepare() returned non-zero");
        }
        int param_cnt = mysql_stmt_param_count(stmt);
        if (param_cnt != 3) {
            failure("Parameter count sanity check failed.");
        }

        memset(bind, 0, sizeof(bind));

        // 1st parameter: valid lifetime
        bind[0].buffer_type = MYSQL_TYPE_LONG;
        bind[0].buffer = &valid_lft;

        // 2nd parameter: cltt
        bind[1].buffer_type = MYSQL_TYPE_STRING;
        bind[1].buffer = &cltt;
        bind[1].buffer_length = cltt_len;

        bind[2].buffer_type = MYSQL_TYPE_LONG;
        bind[2].buffer = &addr;
    }


    for (uint32_t i = 0; i < num_; i++) {

        addr = BASE_ADDR4 + random() % num_;

        if (!compiled_stmt_) {
            char query[128];
            sprintf(query, "UPDATE lease4 SET valid_lft=1002, cltt=now() WHERE addr=%d", addr);
            mysql_real_query(conn_, query, strlen(query));

        } else {
            // compiled statement
            if (mysql_stmt_bind_param(stmt, bind)) {
                failure("Failed to bind parameters: mysql_stmt_bind_param() returned non-zero");
            }

            if (mysql_stmt_execute(stmt)) {
                failure("Failed to execute statement: mysql_stmt_execute() returned non-zero");
            }
        }

        if (verbose_) {
            cout << ".";
        }
    }

    if (compiled_stmt_) {
        if (mysql_stmt_close(stmt)) {
            failure("Failed to close compiled statement, mysql_stmt_close returned non-zero");
        }
    }

    cout << endl;
}

void MySQL_uBenchmark::deleteLease4Test() {
    if (!conn_) {
        throw "Not connected to MySQL server.";
    }

    cout << "DELETE:   ";

    uint32_t addr = 0;

    MYSQL_STMT * stmt = NULL;
    MYSQL_BIND bind[1]; // just a single element
    if (compiled_stmt_) {

        stmt = mysql_stmt_init(conn_);
        if (!stmt) {
            failure("Unable to create compiled statement, mysql_stmt_init() failed");
        }

        const char * statement = "DELETE FROM lease4 WHERE addr=?";

        if (mysql_stmt_prepare(stmt, statement, strlen(statement) )) {
            failure("Failed to prepare statement, mysql_stmt_prepare() returned non-zero");
        }
        int param_cnt = mysql_stmt_param_count(stmt);
        if (param_cnt != 1) {
            failure("Parameter count sanity check failed.");
        }

        memset(bind, 0, sizeof(bind));

        // 1st parameter: IPv4 address
        bind[0].buffer_type = MYSQL_TYPE_LONG;
        bind[0].buffer = (&addr);
        bind[0].is_null = 0;
        bind[0].length = 0;
    }


    for (uint32_t i = 0; i < num_; i++) {

        addr = BASE_ADDR4 + i;

        if (!compiled_stmt_) {
            char query[128];
            sprintf(query, "DELETE FROM lease4 WHERE addr=%d", addr);
            mysql_real_query(conn_, query, strlen(query));
        } else {
            // compiled statement
            if (mysql_stmt_bind_param(stmt, bind)) {
                failure("Failed to bind parameters: mysql_stmt_bind_param() returned non-zero");
            }

            if (mysql_stmt_execute(stmt)) {
                failure("Failed to execute statement: mysql_stmt_execute() returned non-zero");
            }
        }

        if (verbose_) {
            cout << ".";
        }
    }

    if (compiled_stmt_) {
        if (mysql_stmt_close(stmt)) {
            failure("Failed to close compiled statement, mysql_stmt_close returned non-zero");
        }
    }

    cout << endl;
}

void MySQL_uBenchmark::printInfo() {
    cout << "MySQL client version is " << mysql_get_client_info() << endl;
}


int main(int argc, char * const argv[]) {

    const char* hostname ="localhost";  // -m (MySQL server)
    const char* user = "root";          // -u
    const char* passwd = "secret";      // -p
    const char* dbname = "kea";         // -f
    uint32_t num = 100;                 // -n
    bool sync = true;                   // -s
    bool verbose = true;                // -v

    MySQL_uBenchmark bench(hostname, user, passwd, dbname, num, sync, verbose);

    bench.parseCmdline(argc, argv);

    int result = bench.run();

    return (result);
}