/*
 * Copyright (C) Ralph Boehme 2013
 * All Rights Reserved.  See COPYING.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif /* HAVE_CONFIG_H */

#include <stdlib.h>
#include <sys/stat.h>
#include <sys/uio.h>
#include <sys/time.h>
#include <sys/un.h>
#include <sys/socket.h>
#include <sys/param.h>
#include <errno.h>
#include <netinet/in.h>
#include <net/if.h>
#include <netinet/tcp.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <netdb.h>
#include <time.h>
#include <arpa/inet.h>

#include <mysql.h>
#include <mysqld_error.h>
#include <errmsg.h>

#include <atalk/logger.h>
#include <atalk/adouble.h>
#include <atalk/util.h>
#include <atalk/cnid_mysql_private.h>
#include <atalk/cnid_bdb_private.h>
#include <atalk/errchk.h>
#include <atalk/globals.h>

static MYSQL_BIND lookup_param[4], lookup_result[5];
static MYSQL_BIND add_param[4], put_param[5];

/*
 * Prepared statement parameters
 */
static char               stmt_param_name[MAXPATHLEN];
static unsigned long      stmt_param_name_len;
static unsigned long long stmt_param_id;
static unsigned long long stmt_param_did;
static unsigned long long stmt_param_dev;
static unsigned long long stmt_param_ino;

/*
 * lookup result parameters
 */
static unsigned long long lookup_result_id;
static unsigned long long lookup_result_did;
static char               lookup_result_name[MAXPATHLEN];
static unsigned long      lookup_result_name_len;
static unsigned long long lookup_result_dev;
static unsigned long long lookup_result_ino;

static int init_prepared_stmt_lookup(CNID_mysql_private *db)
{
    EC_INIT;
    char *sql = NULL;

    lookup_param[0].buffer_type    = MYSQL_TYPE_STRING;
    lookup_param[0].buffer         = &stmt_param_name;
    lookup_param[0].buffer_length  = sizeof(stmt_param_name);
    lookup_param[0].length         = &stmt_param_name_len;

    lookup_param[1].buffer_type    = MYSQL_TYPE_LONGLONG;
    lookup_param[1].buffer         = &stmt_param_did;
    lookup_param[1].is_unsigned    = true;

    lookup_param[2].buffer_type    = MYSQL_TYPE_LONGLONG;
    lookup_param[2].buffer         = &stmt_param_dev;
    lookup_param[2].is_unsigned    = true;

    lookup_param[3].buffer_type    = MYSQL_TYPE_LONGLONG;
    lookup_param[3].buffer         = &stmt_param_ino;
    lookup_param[3].is_unsigned    = true;

    lookup_result[0].buffer_type   = MYSQL_TYPE_LONGLONG;
    lookup_result[0].buffer        = &lookup_result_id;
    lookup_result[0].is_unsigned   = true;

    lookup_result[1].buffer_type   = MYSQL_TYPE_LONGLONG;
    lookup_result[1].buffer        = &lookup_result_did;
    lookup_result[1].is_unsigned   = true;

    lookup_result[2].buffer_type   = MYSQL_TYPE_STRING;
    lookup_result[2].buffer        = &lookup_result_name;
    lookup_result[2].buffer_length = sizeof(lookup_result_name);
    lookup_result[2].length        = &lookup_result_name_len;

    lookup_result[3].buffer_type   = MYSQL_TYPE_LONGLONG;
    lookup_result[3].buffer        = &lookup_result_dev;
    lookup_result[3].is_unsigned   = true;

    lookup_result[4].buffer_type   = MYSQL_TYPE_LONGLONG;
    lookup_result[4].buffer        = &lookup_result_ino;
    lookup_result[4].is_unsigned   = true;

    EC_NULL( db->cnid_lookup_stmt = mysql_stmt_init(db->cnid_mysql_con) );
    EC_NEG1( asprintf(&sql,
                      "SELECT Id,Did,Name,DevNo,InodeNo FROM %s "
                      "WHERE (Name=? AND Did=?) OR (DevNo=? AND InodeNo=?)",
                      db->cnid_mysql_voluuid_str) );
    EC_ZERO_LOG( mysql_stmt_prepare(db->cnid_lookup_stmt, sql, strlen(sql)) );
    EC_ZERO_LOG( mysql_stmt_bind_param(db->cnid_lookup_stmt, lookup_param) );

EC_CLEANUP:
    if (sql)
        free(sql);
    EC_EXIT;
}

static int init_prepared_stmt_add(CNID_mysql_private *db)
{
    EC_INIT;
    char *sql = NULL;

    EC_NULL( db->cnid_add_stmt = mysql_stmt_init(db->cnid_mysql_con) );
    EC_NEG1( asprintf(&sql,
                      "INSERT INTO %s (Name,Did,DevNo,InodeNo) VALUES(?,?,?,?)",
                      db->cnid_mysql_voluuid_str) );

    add_param[0].buffer_type    = MYSQL_TYPE_STRING;
    add_param[0].buffer         = &stmt_param_name;
    add_param[0].buffer_length  = sizeof(stmt_param_name);
    add_param[0].length         = &stmt_param_name_len;

    add_param[1].buffer_type    = MYSQL_TYPE_LONGLONG;
    add_param[1].buffer         = &stmt_param_did;
    add_param[1].is_unsigned    = true;

    add_param[2].buffer_type    = MYSQL_TYPE_LONGLONG;
    add_param[2].buffer         = &stmt_param_dev;
    add_param[2].is_unsigned    = true;

    add_param[3].buffer_type    = MYSQL_TYPE_LONGLONG;
    add_param[3].buffer         = &stmt_param_ino;
    add_param[3].is_unsigned    = true;

    EC_ZERO_LOG( mysql_stmt_prepare(db->cnid_add_stmt, sql, strlen(sql)) );
    EC_ZERO_LOG( mysql_stmt_bind_param(db->cnid_add_stmt, add_param) );

EC_CLEANUP:
    if (sql)
        free(sql);
    EC_EXIT;
}

static int init_prepared_stmt_put(CNID_mysql_private *db)
{
    EC_INIT;
    char *sql = NULL;

    EC_NULL( db->cnid_put_stmt = mysql_stmt_init(db->cnid_mysql_con) );
    EC_NEG1( asprintf(&sql,
                      "INSERT INTO %s (Id,Name,Did,DevNo,InodeNo) VALUES(?,?,?,?,?)",
                      db->cnid_mysql_voluuid_str) );

    put_param[0].buffer_type    = MYSQL_TYPE_LONGLONG;
    put_param[0].buffer         = &stmt_param_id;
    put_param[0].is_unsigned    = true;

    put_param[1].buffer_type    = MYSQL_TYPE_STRING;
    put_param[1].buffer         = &stmt_param_name;
    put_param[1].buffer_length  = sizeof(stmt_param_name);
    put_param[1].length         = &stmt_param_name_len;

    put_param[2].buffer_type    = MYSQL_TYPE_LONGLONG;
    put_param[2].buffer         = &stmt_param_did;
    put_param[2].is_unsigned    = true;

    put_param[3].buffer_type    = MYSQL_TYPE_LONGLONG;
    put_param[3].buffer         = &stmt_param_dev;
    put_param[3].is_unsigned    = true;

    put_param[4].buffer_type    = MYSQL_TYPE_LONGLONG;
    put_param[4].buffer         = &stmt_param_ino;
    put_param[4].is_unsigned    = true;

    EC_ZERO_LOG( mysql_stmt_prepare(db->cnid_put_stmt, sql, strlen(sql)) );
    EC_ZERO_LOG( mysql_stmt_bind_param(db->cnid_put_stmt, put_param) );

EC_CLEANUP:
    if (sql)
        free(sql);
    EC_EXIT;
}

static int init_prepared_stmt(CNID_mysql_private *db)
{
    EC_INIT;

    EC_ZERO( init_prepared_stmt_lookup(db) );
    EC_ZERO( init_prepared_stmt_add(db) );
    EC_ZERO( init_prepared_stmt_put(db) );

EC_CLEANUP:
    EC_EXIT;
}

static void close_prepared_stmt(CNID_mysql_private *db)
{
    mysql_stmt_close(db->cnid_lookup_stmt);
    mysql_stmt_close(db->cnid_add_stmt);
    mysql_stmt_close(db->cnid_put_stmt);
}

static int cnid_mysql_execute(MYSQL *con, char *fmt, ...)
{
    char *sql = NULL;
    va_list ap;
    int rv;

    va_start(ap, fmt);
    if (vasprintf(&sql, fmt, ap) == -1)
        return -1;
    va_end(ap);

    LOG(log_maxdebug, logtype_cnid, "SQL: %s", sql);

    rv = mysql_query(con, sql);

    if (rv) {
        LOG(log_info, logtype_cnid, "MySQL query \"%s\", error: %s", sql, mysql_error(con));
        errno = CNID_ERR_DB;
    }
    free(sql);
    return rv;
}

int cnid_mysql_delete(struct _cnid_db *cdb, const cnid_t id)
{
    EC_INIT;
    CNID_mysql_private *db;

    if (!cdb || !(db = cdb->_private) || !id) {
        LOG(log_error, logtype_cnid, "cnid_mysql_delete: Parameter error");
        errno = CNID_ERR_PARAM;
        EC_FAIL;
    }

    LOG(log_debug, logtype_cnid, "cnid_mysql_delete(%" PRIu32 "): BEGIN", ntohl(id));
    
    EC_NEG1( cnid_mysql_execute(db->cnid_mysql_con,
                                "DELETE FROM %s WHERE Id=%" PRIu32,
                                db->cnid_mysql_voluuid_str,
                                ntohl(id)) );

    LOG(log_debug, logtype_cnid, "cnid_mysql_delete(%" PRIu32 "): END", ntohl(id));

EC_CLEANUP:
    EC_EXIT;
}

void cnid_mysql_close(struct _cnid_db *cdb)
{
    CNID_mysql_private *db;

    if (!cdb) {
        LOG(log_error, logtype_cnid, "cnid_close called with NULL argument !");
        return;
    }

    if ((db = cdb->_private) != NULL) {
        LOG(log_debug, logtype_cnid, "closing database connection for volume '%s'", db->cnid_mysql_volname);

        free(db->cnid_mysql_voluuid_str);

        close_prepared_stmt(db);

        if (db->cnid_mysql_con)
            mysql_close(db->cnid_mysql_con);
        free(db);
    }

    free(cdb->volpath);
    free(cdb);

    return;
}

int cnid_mysql_update(struct _cnid_db *cdb,
                      cnid_t id,
                      const struct stat *st,
                      cnid_t did,
                      const char *name,
                      size_t len)
{
    EC_INIT;
    CNID_mysql_private *db;
    cnid_t update_id;

    if (!cdb || !(db = cdb->_private) || !id || !st || !name) {
        LOG(log_error, logtype_cnid, "cnid_update: Parameter error");
        errno = CNID_ERR_PARAM;
        EC_FAIL;
    }

    if (len > MAXPATHLEN) {
        LOG(log_error, logtype_cnid, "cnid_update: Path name is too long");
        errno = CNID_ERR_PATH;
        EC_FAIL;
    }

    uint64_t dev = st->st_dev;
    uint64_t ino = st->st_ino;

    do {
        EC_NEG1( cnid_mysql_execute(db->cnid_mysql_con,
                                    "DELETE FROM %s WHERE Id=%" PRIu32,
                                    db->cnid_mysql_voluuid_str,
                                    ntohl(id)) );
        EC_NEG1( cnid_mysql_execute(db->cnid_mysql_con,
                                    "DELETE FROM %s WHERE Did=%" PRIu32 " AND Name='%s'",
                                    db->cnid_mysql_voluuid_str, ntohl(did), name) );
        EC_NEG1( cnid_mysql_execute(db->cnid_mysql_con,
                                    "DELETE FROM %s WHERE DevNo=%" PRIu64 " AND InodeNo=%" PRIu64,
                                    db->cnid_mysql_voluuid_str, dev, ino) );

        stmt_param_id = ntohl(id);
        strncpy(stmt_param_name, name, sizeof(stmt_param_name));
        stmt_param_name_len = len;
        stmt_param_did = ntohl(did);
        stmt_param_dev = dev;
        stmt_param_ino = ino;

        if (mysql_stmt_execute(db->cnid_put_stmt)) {
            switch (mysql_stmt_errno(db->cnid_put_stmt)) {
            case ER_DUP_ENTRY:
                /*
                 * Race condition:
                 * between deletion and insert another process may have inserted
                 * this entry.
                 */
                continue;
            case CR_SERVER_LOST:
                close_prepared_stmt(db);
                EC_ZERO( init_prepared_stmt(db) );
                continue;
            default:
                EC_FAIL;
            }
        }
        update_id = mysql_stmt_insert_id(db->cnid_put_stmt);
    } while (update_id != ntohl(id));

EC_CLEANUP:
    EC_EXIT;
}

cnid_t cnid_mysql_lookup(struct _cnid_db *cdb,
                         const struct stat *st,
                         cnid_t did,
                         const char *name,
                         size_t len)
{
    EC_INIT;
    CNID_mysql_private *db;
    cnid_t id = CNID_INVALID;
    bool have_result = false;

    if (!cdb || !(db = cdb->_private) || !st || !name) {
        LOG(log_error, logtype_cnid, "cnid_mysql_lookup: Parameter error");
        errno = CNID_ERR_PARAM;
        EC_FAIL;
    }

    if (len > MAXPATHLEN) {
        LOG(log_error, logtype_cnid, "cnid_mysql_lookup: Path name is too long");
        errno = CNID_ERR_PATH;
        EC_FAIL;
    }

    uint64_t dev = st->st_dev;
    uint64_t ino = st->st_ino;
    cnid_t hint = db->cnid_mysql_hint;

    LOG(log_maxdebug, logtype_cnid, "cnid_mysql_lookup(did: %" PRIu32 ", name: \"%s\", hint: %" PRIu32 "): START",
        ntohl(did), name, ntohl(hint));

    strncpy(stmt_param_name, name, sizeof(stmt_param_name));
    stmt_param_name_len = len;
    stmt_param_did = ntohl(did);
    stmt_param_dev = dev;
    stmt_param_ino = ino;

exec_stmt:
    if (mysql_stmt_execute(db->cnid_lookup_stmt)) {
        switch (mysql_stmt_errno(db->cnid_lookup_stmt)) {
        case CR_SERVER_LOST:
            close_prepared_stmt(db);
            EC_ZERO( init_prepared_stmt(db) );
            goto exec_stmt;
        default:
            EC_FAIL;
        }
    }
    EC_ZERO_LOG( mysql_stmt_store_result(db->cnid_lookup_stmt) );
    have_result = true;
    EC_ZERO_LOG( mysql_stmt_bind_result(db->cnid_lookup_stmt, lookup_result) );

    uint64_t retdev, retino;
    cnid_t retid, retdid;
    char *retname;

    switch (mysql_stmt_num_rows(db->cnid_lookup_stmt)) {

    case 0:
        /* not found */
        LOG(log_debug, logtype_cnid, "cnid_mysql_lookup: name: '%s', did: %u is not in the CNID database", 
            name, ntohl(did));
        errno = CNID_DBD_RES_NOTFOUND;
        EC_FAIL;

    case 1:
        /* either both OR clauses matched the same id or only one matched, handled below */
        EC_ZERO( mysql_stmt_fetch(db->cnid_lookup_stmt) );
        break;

    case 2:
        /* a mismatch, delete both and return not found */
        while (mysql_stmt_fetch(db->cnid_lookup_stmt) == 0) {
            if (cnid_mysql_delete(cdb, htonl((cnid_t)lookup_result_id))) {
                LOG(log_error, logtype_cnid, "MySQL query error: %s", mysql_error(db->cnid_mysql_con));
                errno = CNID_ERR_DB;
                EC_FAIL;
            }
        }
        errno = CNID_DBD_RES_NOTFOUND;
        EC_FAIL;

    default:
        errno = CNID_ERR_DB;
        EC_FAIL;
    }

    retid = htonl(lookup_result_id);
    retdid = htonl(lookup_result_did);
    retname = lookup_result_name;
    retdev = lookup_result_dev;
    retino = lookup_result_ino;

    if (retdid != did || STRCMP(retname, !=, name)) {
        LOG(log_debug, logtype_cnid,
            "cnid_mysql_lookup(CNID hint: %" PRIu32 ", DID: %" PRIu32 ", name: \"%s\"): server side mv oder reused inode",
            ntohl(hint), ntohl(did), name);
        if (hint != retid) {
            if (cnid_mysql_delete(cdb, retid) != 0) {
                LOG(log_error, logtype_cnid, "MySQL query error: %s", mysql_error(db->cnid_mysql_con));
                errno = CNID_ERR_DB;
                EC_FAIL;
            }
            errno = CNID_DBD_RES_NOTFOUND;
            EC_FAIL;
        }
        LOG(log_debug, logtype_cnid, "cnid_mysql_lookup: server side mv, got hint, updating");
        if (cnid_mysql_update(cdb, retid, st, did, name, len) != 0) {
            LOG(log_error, logtype_cnid, "MySQL query error: %s", mysql_error(db->cnid_mysql_con));
            errno = CNID_ERR_DB;
            EC_FAIL;
        }
        id = retid;
    } else if (retdev != dev || retino != ino) {
        LOG(log_debug, logtype_cnid, "cnid_mysql_lookup(DID:%u, name: \"%s\"): changed dev/ino",
            ntohl(did), name);
        if (cnid_mysql_delete(cdb, retid) != 0) {
            LOG(log_error, logtype_cnid, "MySQL query error: %s", mysql_error(db->cnid_mysql_con));
            errno = CNID_ERR_DB;
            EC_FAIL;
        }
        errno = CNID_DBD_RES_NOTFOUND;
        EC_FAIL;
    } else {
        /* everythings good */
        id = retid;
    }

EC_CLEANUP:
    LOG(log_debug, logtype_cnid, "cnid_mysql_lookup: id: %" PRIu32, ntohl(id));
    if (have_result)
        mysql_stmt_free_result(db->cnid_lookup_stmt);
    if (ret != 0)
        id = CNID_INVALID;
    return id;
}

cnid_t cnid_mysql_add(struct _cnid_db *cdb,
                      const struct stat *st,
                      cnid_t did,
                      const char *name,
                      size_t len,
                      cnid_t hint)
{
    EC_INIT;
    CNID_mysql_private *db;
    cnid_t id = CNID_INVALID;
    MYSQL_RES *result = NULL;
    MYSQL_STMT *stmt;
    my_ulonglong lastid;

    if (!cdb || !(db = cdb->_private) || !st || !name) {
        LOG(log_error, logtype_cnid, "cnid_mysql_add: Parameter error");
        errno = CNID_ERR_PARAM;
        EC_FAIL;
    }

    if (len > MAXPATHLEN) {
        LOG(log_error, logtype_cnid, "cnid_mysql_add: Path name is too long");
        errno = CNID_ERR_PATH;
        EC_FAIL;
    }

    uint64_t dev = st->st_dev;
    uint64_t ino = st->st_ino;
    db->cnid_mysql_hint = hint;

    LOG(log_maxdebug, logtype_cnid, "cnid_mysql_add(did: %" PRIu32 ", name: \"%s\", hint: %" PRIu32 "): START",
        ntohl(did), name, ntohl(hint));

    do {
        if ((id = cnid_mysql_lookup(cdb, st, did, name, len)) == CNID_INVALID) {
            if (errno == CNID_ERR_DB)
                EC_FAIL;
            /*
             * If the CNID set overflowed before (CNID_MYSQL_FLAG_DEPLETED)
             * ignore the CNID "hint" taken from the AppleDouble file
             */
            if (!db->cnid_mysql_hint || (db->cnid_mysql_flags & CNID_MYSQL_FLAG_DEPLETED)) {
                stmt = db->cnid_add_stmt;
            } else {
                stmt = db->cnid_put_stmt;
                stmt_param_id = ntohl(db->cnid_mysql_hint);
            }
            strncpy(stmt_param_name, name, sizeof(stmt_param_name));
            stmt_param_name_len = len;
            stmt_param_did = ntohl(did);
            stmt_param_dev = dev;
            stmt_param_ino = ino;

            if (mysql_stmt_execute(stmt)) {
                switch (mysql_stmt_errno(stmt)) {
                case ER_DUP_ENTRY:
                    break;
                case CR_SERVER_LOST:
                    close_prepared_stmt(db);
                    EC_ZERO( init_prepared_stmt(db) );
                    continue;
                default:
                    EC_FAIL;
                }
                /*
                 * Race condition:
                 * between lookup and insert another process may have inserted
                 * this entry.
                 */
                if (db->cnid_mysql_hint)
                    db->cnid_mysql_hint = CNID_INVALID;
                continue;
            }

            lastid = mysql_stmt_insert_id(stmt);

            if (lastid > 0xffffffff) {
                /* CNID set ist depleted, restart from scratch */
                EC_NEG1( cnid_mysql_execute(db->cnid_mysql_con,
                                            "START TRANSACTION;"
                                            "UPDATE volumes SET Depleted=1 WHERE VolUUID='%s';"
                                            "TRUNCATE TABLE %s;"
                                            "ALTER TABLE %s AUTO_INCREMENT = 17;" 
                                            "COMMIT;",
                                            db->cnid_mysql_voluuid_str,
                                            db->cnid_mysql_voluuid_str,
                                            db->cnid_mysql_voluuid_str) );
                db->cnid_mysql_flags |= CNID_MYSQL_FLAG_DEPLETED;
                hint = CNID_INVALID;
                do {
                    result = mysql_store_result(db->cnid_mysql_con);
                    if (result)
                        mysql_free_result(result);
                } while (mysql_next_result(db->cnid_mysql_con) == 0);
                continue;
            }

            /* Finally assign our result */
            id = htonl((uint32_t)lastid);
        }
    } while (id == CNID_INVALID);

EC_CLEANUP:
    LOG(log_debug, logtype_cnid, "cnid_mysql_add: id: %" PRIu32, ntohl(id));

    if (result)
        mysql_free_result(result);
    return id;
}

cnid_t cnid_mysql_get(struct _cnid_db *cdb, cnid_t did, const char *name, size_t len)
{
    EC_INIT;
    CNID_mysql_private *db;
    cnid_t id = CNID_INVALID;
    MYSQL_RES *result = NULL;
    MYSQL_ROW row;

    if (!cdb || !(db = cdb->_private) || !name) {
        LOG(log_error, logtype_cnid, "cnid_mysql_get: Parameter error");
        errno = CNID_ERR_PARAM;
        EC_FAIL;
    }

    if (len > MAXPATHLEN) {
        LOG(log_error, logtype_cnid, "cnid_mysql_get: name is too long");
        errno = CNID_ERR_PATH;
        return CNID_INVALID;
    }

    LOG(log_debug, logtype_cnid, "cnid_mysql_get(did: %" PRIu32 ", name: \"%s\"): START",
        ntohl(did),name);

    EC_NEG1( cnid_mysql_execute(db->cnid_mysql_con,
                                "SELECT Id FROM %s "
                                "WHERE Name='%s' AND Did=%" PRIu32,
                                db->cnid_mysql_voluuid_str,
                                name,
                                ntohl(did)) );

    if ((result = mysql_store_result(db->cnid_mysql_con)) == NULL) {
        LOG(log_error, logtype_cnid, "MySQL query error: %s", mysql_error(db->cnid_mysql_con));
        errno = CNID_ERR_DB;
        EC_FAIL;
    }

    if (mysql_num_rows(result)) {
        row = mysql_fetch_row(result);
        id = htonl(atoi(row[0]));
    }

EC_CLEANUP:
    LOG(log_debug, logtype_cnid, "cnid_mysql_get: id: %" PRIu32, ntohl(id));

    if (result)
        mysql_free_result(result);

    return id;
}

char *cnid_mysql_resolve(struct _cnid_db *cdb, cnid_t *id, void *buffer, size_t len)
{
    EC_INIT;
    CNID_mysql_private *db;
    MYSQL_RES *result = NULL;
    MYSQL_ROW row;

    if (!cdb || !(db = cdb->_private)) {
        LOG(log_error, logtype_cnid, "cnid_mysql_get: Parameter error");
        errno = CNID_ERR_PARAM;
        EC_FAIL;
    }

    EC_NEG1( cnid_mysql_execute(
                 db->cnid_mysql_con,
                 "SELECT Did,Name FROM %s WHERE Id=%" PRIu32,
                 db->cnid_mysql_voluuid_str, ntohl(*id)) );

    EC_NULL( result = mysql_store_result(db->cnid_mysql_con) );

    if (mysql_num_rows(result) != 1)
        EC_FAIL;

    row = mysql_fetch_row(result);

    *id = htonl(atoi(row[0]));
    strncpy(buffer, row[1], len);

EC_CLEANUP:             
    if (result)
        mysql_free_result(result);

    if (ret != 0) {
        *id = CNID_INVALID;
        return NULL;
    }
    return buffer;
}

/**
 * Caller passes buffer where we will store the db stamp
 **/
int cnid_mysql_getstamp(struct _cnid_db *cdb, void *buffer, const size_t len)
{
    EC_INIT;
    CNID_mysql_private *db;
    MYSQL_RES *result = NULL;
    MYSQL_ROW row;

    if (!cdb || !(db = cdb->_private)) {
        LOG(log_error, logtype_cnid, "cnid_find: Parameter error");
        errno = CNID_ERR_PARAM;
        return CNID_INVALID;
    }

    if (!buffer)
        EC_EXIT_STATUS(0);

    if (cnid_mysql_execute(db->cnid_mysql_con,
                           "SELECT Stamp FROM volumes WHERE VolPath='%s'",
                           db->cnid_mysql_volname)) {
        if (mysql_errno(db->cnid_mysql_con) != ER_DUP_ENTRY) {
            LOG(log_error, logtype_cnid, "MySQL query error: %s", mysql_error(db->cnid_mysql_con));
            EC_FAIL;
        }
    }

    if ((result = mysql_store_result(db->cnid_mysql_con)) == NULL) {
        LOG(log_error, logtype_cnid, "MySQL query error: %s", mysql_error(db->cnid_mysql_con));
        errno = CNID_ERR_DB;
        EC_FAIL;
    }
    if (!mysql_num_rows(result)) {
        LOG(log_error, logtype_cnid, "Can't get DB stamp for volumes \"%s\"", db->cnid_mysql_volname);
        EC_FAIL;
    }
    row = mysql_fetch_row(result);
    memcpy(buffer, row[0], len);

EC_CLEANUP:
    if (result)
        mysql_free_result(result);
    EC_EXIT;
}


int cnid_mysql_find(struct _cnid_db *cdb, const char *name, size_t namelen, void *buffer, size_t buflen)
{
    LOG(log_error, logtype_cnid,
        "cnid_mysql_find(\"%s\"): not supported with MySQL CNID backend", name);
    return -1;
}

cnid_t cnid_mysql_rebuild_add(struct _cnid_db *cdb, const struct stat *st,
                              cnid_t did, const char *name, size_t len, cnid_t hint)
{
    LOG(log_error, logtype_cnid,
        "cnid_mysql_rebuild_add(\"%s\"): not supported with MySQL CNID backend", name);
    return CNID_INVALID;
}

int cnid_mysql_wipe(struct _cnid_db *cdb)
{
    EC_INIT;
    CNID_mysql_private *db;
    MYSQL_RES *result = NULL;

    if (!cdb || !(db = cdb->_private)) {
        LOG(log_error, logtype_cnid, "cnid_wipe: Parameter error");
        errno = CNID_ERR_PARAM;
        return -1;
    }

    LOG(log_debug, logtype_cnid, "cnid_dbd_wipe");

    EC_NEG1( cnid_mysql_execute(
                 db->cnid_mysql_con,
                 "START TRANSACTION;"
                 "UPDATE volumes SET Depleted=0 WHERE VolUUID='%s';"
                 "TRUNCATE TABLE %s;"
                 "ALTER TABLE %s AUTO_INCREMENT = 17;"
                 "COMMIT;",
                 db->cnid_mysql_voluuid_str,
                 db->cnid_mysql_voluuid_str,
                 db->cnid_mysql_voluuid_str) );

    do {
        result = mysql_store_result(db->cnid_mysql_con);
        if (result)
            mysql_free_result(result);
    } while (mysql_next_result(db->cnid_mysql_con) == 0);

EC_CLEANUP:
    EC_EXIT;
}

static struct _cnid_db *cnid_mysql_new(const char *volpath)
{
    struct _cnid_db *cdb;

    if ((cdb = (struct _cnid_db *)calloc(1, sizeof(struct _cnid_db))) == NULL)
        return NULL;

    if ((cdb->volpath = strdup(volpath)) == NULL) {
        free(cdb);
        return NULL;
    }

    cdb->flags = CNID_FLAG_PERSISTENT | CNID_FLAG_LAZY_INIT;
    cdb->cnid_add = cnid_mysql_add;
    cdb->cnid_delete = cnid_mysql_delete;
    cdb->cnid_get = cnid_mysql_get;
    cdb->cnid_lookup = cnid_mysql_lookup;
    cdb->cnid_find = cnid_mysql_find;
    cdb->cnid_nextid = NULL;
    cdb->cnid_resolve = cnid_mysql_resolve;
    cdb->cnid_getstamp = cnid_mysql_getstamp;
    cdb->cnid_update = cnid_mysql_update;
    cdb->cnid_rebuild_add = cnid_mysql_rebuild_add;
    cdb->cnid_close = cnid_mysql_close;
    cdb->cnid_wipe = cnid_mysql_wipe;
    return cdb;
}

static const char *printuuid(const unsigned char *uuid) {
    static char uuidstring[64];
    const char *uuidmask;
    int i = 0;
    unsigned char c;

    while ((c = *uuid)) {
        uuid++;
        sprintf(uuidstring + i, "%02X", c);
        i += 2;
    }
    uuidstring[i] = 0;
    return uuidstring;
}

/* ---------------------- */
struct _cnid_db *cnid_mysql_open(struct cnid_open_args *args)
{
    EC_INIT;
    CNID_mysql_private *db = NULL;
    struct _cnid_db *cdb = NULL;
    MYSQL_RES *result = NULL;
    MYSQL_ROW row;

    EC_NULL( cdb = cnid_mysql_new(args->dir) );
    EC_NULL( db = (CNID_mysql_private *)calloc(1, sizeof(CNID_mysql_private)) );
    cdb->_private = db;

    db->cnid_mysql_volname = strdup(args->dir); /* db_dir contains the volume name */
    db->cnid_mysql_magic = CNID_DB_MAGIC;
    db->cnid_mysql_obj = args->obj;
    memcpy(db->cnid_mysql_voluuid, args->voluuid, sizeof(atalk_uuid_t));
    db->cnid_mysql_voluuid_str = strdup(printuuid(db->cnid_mysql_voluuid));

    /* Initialize and connect to MySQL server */
    EC_NULL( db->cnid_mysql_con = mysql_init(NULL) );
    my_bool my_recon = true;
    EC_ZERO( mysql_options(db->cnid_mysql_con, MYSQL_OPT_RECONNECT, &my_recon) );
    int my_timeout = 600;
    EC_ZERO( mysql_options(db->cnid_mysql_con, MYSQL_OPT_CONNECT_TIMEOUT, &my_timeout) );

    const AFPObj *obj = db->cnid_mysql_obj;

    EC_NULL( mysql_real_connect(db->cnid_mysql_con,
                                obj->options.cnid_mysql_host,
                                obj->options.cnid_mysql_user,
                                obj->options.cnid_mysql_pw,
                                obj->options.cnid_mysql_db,
                                0, NULL, CLIENT_MULTI_STATEMENTS));

    /* Add volume to volume table */
    if (cnid_mysql_execute(db->cnid_mysql_con,
                           "CREATE TABLE IF NOT EXISTS volumes"
                           "(VolUUID CHAR(32) PRIMARY KEY,VolPath TEXT(4096),Stamp BINARY(8),Depleted INT, INDEX(VolPath(64)))")) {
        LOG(log_error, logtype_cnid, "MySQL query error: %s", mysql_error(db->cnid_mysql_con));
        EC_FAIL;
    }
    time_t now = time(NULL);
    char stamp[8];
    memset(stamp, 0, 8);
    memcpy(stamp, &now, sizeof(time_t));
    char blob[16+1];
    mysql_real_escape_string(db->cnid_mysql_con, blob, stamp, 8);

    if (cnid_mysql_execute(db->cnid_mysql_con,
                           "INSERT INTO volumes (VolUUID,Volpath,Stamp,Depleted) "
                           "VALUES('%s','%s','%s',0)",
                           db->cnid_mysql_voluuid_str,
                           db->cnid_mysql_volname,
                           blob)) {
        if (mysql_errno(db->cnid_mysql_con) != ER_DUP_ENTRY) {
            LOG(log_error, logtype_cnid, "MySQL query error: %s", mysql_error(db->cnid_mysql_con));
            EC_FAIL;
        }
    }

    /*
     * Check whether CNID set overflowed before.
     * If that's the case, in cnid_mysql_add() we'll ignore the CNID "hint" taken from the
     * AppleDouble file.
     */
    if (cnid_mysql_execute(db->cnid_mysql_con,
                           "SELECT Depleted FROM volumes WHERE VolUUID='%s'",
                           db->cnid_mysql_voluuid_str)) {
        LOG(log_error, logtype_cnid, "MySQL query error: %s", mysql_error(db->cnid_mysql_con));
        EC_FAIL;
    }
    if ((result = mysql_store_result(db->cnid_mysql_con)) == NULL) {
        LOG(log_error, logtype_cnid, "MySQL query error: %s", mysql_error(db->cnid_mysql_con));
        errno = CNID_ERR_DB;
        EC_FAIL;
    }
    if (mysql_num_rows(result)) {
        row = mysql_fetch_row(result);
        int depleted = atoi(row[0]);
        if (depleted)
            db->cnid_mysql_flags |= CNID_MYSQL_FLAG_DEPLETED;
    }
    mysql_free_result(result);
    result = NULL;

    /* Create volume table */
    if (cnid_mysql_execute(db->cnid_mysql_con,
                           "CREATE TABLE IF NOT EXISTS %s"
                           "(Id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,"
                           "Name VARCHAR(255) NOT NULL,"
                           "Did INT UNSIGNED NOT NULL,"
                           "DevNo BIGINT UNSIGNED NOT NULL,"
                           "InodeNo BIGINT UNSIGNED NOT NULL,"
                           "UNIQUE DidName(Did, Name), UNIQUE DevIno(DevNo, InodeNo)) "
                           "AUTO_INCREMENT=17",
                           db->cnid_mysql_voluuid_str)) {
        LOG(log_error, logtype_cnid, "MySQL query error: %s", mysql_error(db->cnid_mysql_con));
        EC_FAIL;
    }

    EC_ZERO( init_prepared_stmt(db) );

    LOG(log_debug, logtype_cnid, "Finished initializing MySQL CNID module for volume '%s'",
        db->cnid_mysql_volname);

EC_CLEANUP:
    if (result)
        mysql_free_result(result);
    if (ret != 0) {
        if (cdb) {
            if (cdb->volpath != NULL) {
                free(cdb->volpath);
            }
            free(cdb);
        }
        cdb = NULL;
        if (db)
            free(db);
    }
    return cdb;
}

struct _cnid_module cnid_mysql_module = {
    "mysql",
    {NULL, NULL},
    cnid_mysql_open,
    0
};
