#include "stdio.h"
#include "stdlib.h"
#include "string.h"
#include <my_global.h>
#include <my_sys.h>
#include <mysql.h>

int exit_failure(const char *msg, MYSQL *mysql, MYSQL_RES *res);

/* Connection parameter - change */
const char *con_host = "localhost";
const char *con_user = "root";
const char *con_pass = "root";
const char *con_db = "test";
unsigned int con_port = 3306;
const char *con_socket = "/tmp/mysql.sock";
unsigned long con_flags = 0;

MYSQL *conn;
MYSQL_RES *res;
MYSQL_ROW row;

int main(void) {

	printf("\n");
	printf("Connecting...\n");

	conn = mysql_init(NULL);
	if (conn == NULL)
		exit_failure("mysql_init() failed", NULL, NULL);

	if (mysql_real_connect(conn, con_host, con_user, con_pass, con_db, con_port, con_socket, con_flags) == NULL)
		exit_failure("mysql_real_connect() failed", conn, NULL);

	printf("Server: 	%s\n", mysql_get_server_info(conn));
	printf("Protocol: 	%u\n", mysql_get_proto_info(conn));
	printf("Client: 	%s\n\n", mysql_get_client_info());

	/*
	This will *block* when you execute this program more than
	once without restarting the MySQL Server between program invocation.
	This is the actual bug.
	*/
	printf("SELECT IS_USED_LOCK('lock_never_set') - lock gets never set, ergo this should never block...\n");
	if (mysql_query(conn, "SELECT IS_USED_LOCK('lock_never_set')") != 0)
		exit_failure("First IS_USED_LOCK() failed", conn, NULL);

	res = mysql_store_result(conn);
	if (res == NULL)
		exit_failure("mysql_store_result() - lock_never_set", conn, NULL);

	if ((row = mysql_fetch_row(res)) == NULL)
		exit_failure("mysql_fetch_row() - lock_never_set", conn, res);

	if (row[0] != NULL)
		exit_failure("User lock 'lock_never_set' is already set.", conn, res);

	mysql_free_result(res);

	/*
	Now we start to put the MySQL Server in a state that
	no other user will be able to use any function related to user locks
	before restarting the MySQL Server.

	We acquire a user lock. This user lock will not be released by the client
	using RELEASE_LOCK(). Instead the client will rely on mysql_change_user()
	to free the lock, see also http://dev.mysql.com/doc/refman/5.1/en/mysql-change-user.html
	As we'll see this seems to work fine...
	*/
	printf("SELECT GET_LOCK('change_user_lock', 10) - checking if change_user will release it implicitly...\n");
	if (mysql_query(conn, "SELECT GET_LOCK('change_user_lock', 10)") != 0)
		exit_failure("GET_LOCK('change_user_lock') failed", conn, NULL);

	res = mysql_store_result(conn);
	if (res == NULL)
		exit_failure("mysql_store_result() - change_user_lock", conn, NULL);

	if ((row = mysql_fetch_row(res)) == NULL)
		exit_failure("mysql_fetch_row() - change_user_lock", conn, res);

	if (row[0] == NULL)
		exit_failure("Cannot acquire user lock 'change_user_lock'.", conn, res);

	printf("SELECT GET_LOCK('change_user_lock') --> %s ...\n", row[0]);
	if (strncmp(row[0], "1", 1))
		exit_failure("GET_LOCK('change_user_lock') did not return '1' to indicate success", conn, res);

	mysql_free_result(res);

	/*
	Cool, we got the user lock 'change_user_lock'. Now we call
	change_user_lock() and expect the server to clean up the connection.
	That includes, among others:
		- ROLLBACK
  	- release all locks acquired with GET_LOCK()
 		- ...

	We test the lock release with:
		SELECT IS_USED_LOCK('change_user_lock') => expected: NULL
		SELECT IS_FREE_LOCK('change_user_lock') => expected: 1

	*/
	printf("Calling change_user() ...\n");
	if (mysql_change_user(conn, con_user, con_pass, con_db) != 0)
		exit_failure("mysql_change_user() failed", conn, NULL);

	printf("Checking if the user lock has been released due to change_user() ...\n");
	if (mysql_query(conn, "SELECT IS_USED_LOCK('change_user_lock')") != 0)
		exit_failure("SELECT IS_USED_LOCK('change_user_lock') failed", conn, NULL);

	res = mysql_store_result(conn);
	if (res == NULL)
		exit_failure("mysql_store_result() - change_user_lock", conn, NULL);

	if ((row = mysql_fetch_row(res)) == NULL)
		exit_failure("mysql_fetch_row() - change_user_lock", conn, res);

	if (row[0] != NULL)
		exit_failure("IS_USED_LOCK('change_user_lock') indicates that the lock has not been released after change_user()!", conn, res);

	mysql_free_result(res);
	printf("SELECT IS_USED_LOCK('change_user_lock') --> NULL ...\n");

	printf("IS_FREE_LOCK('change_user_lock')? ...\n");
	if (mysql_query(conn, "SELECT IS_FREE_LOCK('change_user_lock')") != 0)
		exit_failure("SELECT IS_FREE_LOCK('change_user_lock') failed", conn, NULL);

	res = mysql_store_result(conn);
	if (res == NULL)
		exit_failure("mysql_store_result() - change_user_lock", conn, NULL);

	if ((row = mysql_fetch_row(res)) == NULL)
		exit_failure("mysql_fetch_row() - change_user_lock", conn, res);

	if (row[0] == NULL ||
		strncmp(row[0], "1", 1))
		exit_failure("IS_FREE_LOCK('change_user_lock') indicates that the lock has not been released after change_user()!", conn, res);
	printf("SELECT IS_FREE_LOCK('change_user_lock') --> %s ...\n", row[0]);
	mysql_free_result(res);

	mysql_close(conn);

	/*
	Great, the MySQL Server has released the user locks.
	Does mysql_change_user() work fine? Well, let's re-run the test...
	*/
	printf("... done!\n\n");
	printf("Re-run the program and check if the very first statement\n");
	printf("SELECT IS_USED_LOCK('lock_never_set') will block.\n");
	printf("As at no point a user lock 'lock_never_set' has been set,\n");
	printf("statement must not block. If it blocks, check SHOW PROCESSLIST.\n");

	return EXIT_SUCCESS;
}

int exit_failure(const char *msg, MYSQL *mysql, MYSQL_RES *res) {
	printf("ERROR: %s\n", msg);
	if (mysql) {
		printf("[%u] %s\n", mysql_errno(mysql), mysql_error(mysql));
		if (res)
			mysql_free_result(res);
		mysql_close(mysql);
	}
	printf("\n");
	exit(EXIT_FAILURE);
}
