#include "mod_odbc.h"

static SQLHENV henv = SQL_NULL_HENV; /* Environment Handle */

static size_t string_apply_maxlen (const char * const string, const size_t len, const size_t maxlen)
{
	static const uint8_t numbytes_utf8[256] = {
		1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
		1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
		1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
		1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
		1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
		1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
		2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
		3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, 4,4,4,4,4,4,4,4,5,5,5,5,6,6,6,6
	};

	size_t i = 0;
	size_t chars = 0;
	do {
		uint8_t	numbytes = numbytes_utf8[(uint8_t)string[i]];

		i += numbytes;

		if (i > len)
			return 0;

		if (++chars == maxlen)
			return i;
	} while (i != len);

	return i;
}

#ifdef WIN32
static size_t utf8_to_ucs2 (struct modctx *ctx, char *string)
{
	const size_t inlen = strlen (string);
	const size_t outlen = inlen * 2;

	if (outlen > ctx->string2len) {
		free (ctx->string2);
		ctx->string2 = malloc (outlen);
		ctx->string2len = outlen;
	}

	return MultiByteToWideChar (CP_UTF8, 0, string, (int)inlen, (LPWSTR)ctx->string2, ctx->string2len / 2) * 2;
}
#else
static size_t utf8_to_ucs2 (struct modctx *ctx, char *string)
{
	size_t inbytes_left = strlen (string);
	size_t outbytes_left = inbytes_left * 2;

	if (outbytes_left > ctx->string2len) {
		free (ctx->string2);
		ctx->string2 = malloc (outbytes_left);
		ctx->string2len = outbytes_left;
	} else {
		outbytes_left = ctx->string2len;
	}

	char *inbuf = string;
	char *outbuf = ctx->string2;

	iconv (ctx->iconv_cd, NULL, NULL, NULL, NULL);

	if (iconv (ctx->iconv_cd, &inbuf, &inbytes_left, &outbuf, &outbytes_left) == (size_t) -1)
		return 0;

	return ctx->string2len - outbytes_left;
}
#endif

static int extract_error (SQLHANDLE handle, SQLSMALLINT type, struct modctx *ctx)
{
	if (ctx->odbcerrstr == NULL) {
		ctx->odbcerrstr = malloc (128);
		ctx->odbcerrstr_len = 128;
	}

	SQLINTEGER i = 0;
	SQLINTEGER native;
	SQLCHAR state[6];
	SQLSMALLINT len;
	SQLRETURN ret;

	int catched_error = 0;
	int rc = -4;

	do {
		ret = SQLGetDiagRec (type, handle, ++i, state, &native, ctx->odbcerrstr, ctx->odbcerrstr_len, &len);

//		if (ret != SQL_NO_DATA)
//			fprintf (stderr, "SQLGetDiagRec=%d state=%s err=%s\n", ret, state, ctx->odbcerrstr);

		if (ret == SQL_SUCCESS_WITH_INFO) {
			free (ctx->odbcerrstr);
			ctx->odbcerrstr = malloc (len + 1);
			ctx->odbcerrstr_len = len + 1;
			i--;
			ret = SQL_SUCCESS; /* prevent the do-while loop from terminating */
		} else if (ret == SQL_SUCCESS && catched_error == 0) { /* we are only interested in the first error string */
			if (memcmp ((char *)state, "08", 2) == 0) /* Class "08" Connection Exception */
				rc = -2;
			else if (memcmp ((char *)state, "23", 2) == 0) /* Class "23" Integrity Constraint Violation */
				rc = -1;
			else if (memcmp ((char *)state, "25", 2) == 0) /* Class "25" Invalid Transaction State */
				rc = -3;
			else if (memcmp ((char *)state, "40", 2) == 0) /* Class "40" Transaction Rollback */
				rc = -3;
			else if (strcmp ((char *)state, "XA100") == 0) /* MariaDB */
				rc = -3;
			else if (strcmp ((char *)state, "XA102") == 0) /* MariaDB */
				rc = -3;
			else if (strcmp ((char *)state, "XA106") == 0) /* MariaDB */
				rc = -3;
			else if (strcmp ((char *)state, "HYT00") == 0)
				rc = -2;

			free (ctx->errorstring);
			ctx->errorstring = malloc (len + 9);
			sprintf (ctx->errorstring, "%s (%.5s)", ctx->odbcerrstr, state);

			ctx->nativeerror = native;
			catched_error = 1;
		}
	} while (ret == SQL_SUCCESS);

	return rc;
}

/* Global Module Init - Called once per MDA process upon loading the module */
int mod_init ()
{
	SQLRETURN ret = SQLAllocHandle (SQL_HANDLE_ENV, SQL_NULL_HANDLE, &henv);

	if (ret == SQL_INVALID_HANDLE || ret == SQL_ERROR)
		return 0;

	ret = SQLSetEnvAttr (henv, SQL_ATTR_ODBC_VERSION, (SQLPOINTER*)SQL_OV_ODBC3, 0);

	if (ret == SQL_INVALID_HANDLE || ret == SQL_ERROR) {
		SQLFreeHandle (SQL_HANDLE_ENV, henv);
		henv = SQL_NULL_HENV;
		return 0;
	}

	return 1;
}

/* Global Module Exit - Called last once per MDA process upon unloading the module */
int mod_exit ()
{
	if (henv != SQL_NULL_HENV) {
		SQLFreeHandle (SQL_HANDLE_ENV, henv);
		henv = SQL_NULL_HENV;
	}

	return 1;
}

/* Thread Module Init - Called once per Queue thread upon creation */
void *mod_create (mod_config *cfg)
{
	struct modctx *ctx = calloc (1, sizeof *ctx);

	if (ctx == NULL)
		return NULL;

	ctx->hdbc = SQL_NULL_HDBC;
	ctx->hstmt = SQL_NULL_HSTMT;
	ctx->cfg = cfg;
#ifndef WIN32
	ctx->iconv_cd = iconv_open ("UCS-2//IGNORE", "UTF-8");
#endif
	return ctx;
}

/* Thread Module Exit - Called once per Queue thread upon exit */
int mod_destroy (void *handle)
{
	struct modctx *ctx = handle;

	if (ctx == NULL)
		return 0;

	if (ctx->hstmt != SQL_NULL_HSTMT) {
		SQLFreeHandle (SQL_HANDLE_STMT, ctx->hstmt);
		ctx->hstmt = SQL_NULL_HSTMT;
	}

	if (ctx->hdbc != SQL_NULL_HDBC) {
		SQLDisconnect (ctx->hdbc);
		SQLFreeHandle (SQL_HANDLE_DBC, ctx->hdbc);
		ctx->hdbc = SQL_NULL_HDBC;
	}

	mod_freeresult (ctx);

	free (ctx->odbcerrstr);
	free (ctx->errorstring);
	free (ctx->string);
	free (ctx->string2);
#ifndef WIN32
	iconv_close (ctx->iconv_cd);
#endif
	free (ctx);

	return 1;
}

int mod_connect (void *handle)
{
	struct modctx *ctx = handle;

	if (ctx == NULL)
		return 0;

	free (ctx->errorstring);
	ctx->errorstring = NULL;
	ctx->nativeerror = 0;

	SQLRETURN ret;

	if (ctx->hdbc == SQL_NULL_HDBC) {
		ret = SQLAllocHandle (SQL_HANDLE_DBC, henv, &ctx->hdbc);

		if (!SQL_SUCCEEDED (ret)) {
			extract_error (ctx->hdbc, SQL_HANDLE_DBC, ctx);
			return 0;
		}
	}

	if (ctx->cfg->connectionstring == NULL) {
		ctx->errorstring = strdup ("A Connection String is required for this module");
		return 0;
	}

	/* it turned out that e.g MariaDB didn't enable UTF-8 unless SQLDriverConnectW() was used over SQLDriverConnect() */
	const size_t len = utf8_to_ucs2 (ctx, ctx->cfg->connectionstring);

	if (len == 0) {
		ctx->errorstring = strdup ("Error converting the UTF-8 connectionstring to UTF-16");
		return 0;
	}

	ret = SQLDriverConnectW (ctx->hdbc, NULL, (SQLWCHAR *)ctx->string2, len / 2, NULL, 0, NULL, SQL_DRIVER_NOPROMPT);

	if (!SQL_SUCCEEDED (ret)) {
		extract_error (ctx->hdbc, SQL_HANDLE_DBC, ctx);
		return 0;
	}

	if (ctx->hstmt == SQL_NULL_HSTMT) {
		ret = SQLAllocHandle (SQL_HANDLE_STMT, ctx->hdbc, &ctx->hstmt);

		if (!SQL_SUCCEEDED (ret)) {
			extract_error (ctx->hdbc, SQL_HANDLE_DBC, ctx);
			return 0;
		}
	}

	if (ctx->cfg->use_transactions == 1) {
		ret = SQLSetConnectAttr (ctx->hdbc, SQL_ATTR_AUTOCOMMIT, SQL_AUTOCOMMIT_OFF, 0);

		if (!SQL_SUCCEEDED (ret)) {
			extract_error (ctx->hdbc, SQL_HANDLE_DBC, ctx);
			fprintf (stderr, "Unable to disable autocommit: %s\n", ctx->errorstring);
		}
	}

	/* make sure that we get version and type of the sql server for all threads */
	if (ctx->sqlserver_major == 0 && ctx->sqlserver_minor == 0 && ctx->sqlserver_patch == 0)
		mod_get_info (ctx, NULL, sizeof (mod_info));

	if (ctx->sqlserver == MSSQL) {
		if (mod_executesql (ctx, "SET NOCOUNT OFF") != 0)
			fprintf (stderr, "SET NOCOUNT OFF failed: %s\n", ctx->errorstring);

		if (mod_executesql (ctx, "SET XACT_ABORT OFF") != 0)
			fprintf (stderr, "SET XACT_ABORT OFF failed: %s\n", ctx->errorstring);
	}

	return 1;
}

int mod_disconnect (void *handle)
{
	struct modctx *ctx = handle;

	if (ctx == NULL)
		return 0;

	if (ctx->hstmt != SQL_NULL_HSTMT) {
		if (ctx->cfg->use_transactions == 1)
			SQLEndTran (SQL_HANDLE_DBC, ctx->hdbc, SQL_COMMIT);

		SQLFreeHandle (SQL_HANDLE_STMT, ctx->hstmt);

		SQLDisconnect (ctx->hdbc);
		ctx->hstmt = SQL_NULL_HSTMT;
	}

	return 1;
}

void mod_reconnect (void *handle)
{
	struct modctx *ctx = handle;

	if (ctx == NULL)
		return;

	if (ctx->status != MOD_STATUS_SHUTDOWN)
		ctx->status = MOD_STATUS_DISCONNECTED;

	while (ctx->status == MOD_STATUS_DISCONNECTED) {
		mod_disconnect (ctx);

		if (mod_connect (handle) == 1) {
			if (ctx->status != MOD_STATUS_SHUTDOWN)
				ctx->status = MOD_STATUS_OK;

			return;
		}
#ifndef WIN32
		sleep (1);
#else
		Sleep (1000);
#endif
	}
}

int mod_getlasterror (void *handle)
{
	struct modctx *ctx = handle;

	if (ctx == NULL)
		return -1;

	return ctx->nativeerror;
}

char *mod_geterrorstring (void *handle)
{
	struct modctx *ctx = handle;

	if (ctx == NULL)
		return NULL;

	return ctx->errorstring;
}

void mod_freeresult (void *handle)
{
	struct modctx *ctx = handle;

	if (ctx == NULL)
		return ;

	while (ctx->result_head != NULL) {
		mod_result *next = ctx->result_head->next;

		free (ctx->result_head->columnvalue);
		free (ctx->result_head);

		ctx->result_head = next;
	}

	ctx->result_tail = NULL;

	return;
}

void *mod_querysql (void *handle, char *query, int *returncode)
{
//	fprintf (stderr, "QuerySql: '%s'\n", query);
	struct modctx *ctx = handle;

	if (ctx == NULL)
		return NULL;

	mod_freeresult (ctx);

	free (ctx->errorstring);
	ctx->errorstring = NULL;
	ctx->nativeerror = 0;

	SQLFreeStmt (ctx->hstmt, SQL_CLOSE);

	const size_t len = utf8_to_ucs2 (ctx, query);

	if (len == 0) {
		ctx->errorstring = strdup ("Error converting the UTF-8 query to UTF-16");
		*returncode = -1;
		return NULL;
	}

	SQLRETURN ret = SQLExecDirectW (ctx->hstmt, (SQLWCHAR *) ctx->string2, len / 2);

	if (!SQL_SUCCEEDED (ret)) {
		*returncode = extract_error (ctx->hstmt, SQL_HANDLE_STMT, ctx);
		return NULL;
	}

	SQLSMALLINT columns;

	ret = SQLNumResultCols (ctx->hstmt, &columns);

	if (!SQL_SUCCEEDED (ret)) {
		*returncode = extract_error (ctx->hstmt, SQL_HANDLE_STMT, ctx);

		/* just in case */
		if (*returncode == -1)
			*returncode = -4;

		return NULL;
	}

	do {
		ret = SQLFetch (ctx->hstmt);

		if (SQL_SUCCEEDED (ret)) {
			SQLUSMALLINT col;
			for (col = 1; col <= columns; col++) {
				if (ctx->result_tail == NULL) {
					ctx->result_tail = calloc (1, sizeof *ctx->result_tail);
					ctx->result_head = ctx->result_tail;
				} else {
					ctx->result_tail->next = calloc (1, sizeof *ctx->result_tail);
					ctx->result_tail = ctx->result_tail->next;
				}

				SQLLEN sqlen;
				char buf[1024];

				ret = SQLGetData (ctx->hstmt, col, SQL_C_CHAR, buf, sizeof buf, &sqlen);

				if (SQL_SUCCEEDED (ret) && sqlen != SQL_NULL_DATA)
					ctx->result_tail->columnvalue = strdup (buf);
			}
		}
	} while (ret != SQL_NO_DATA_FOUND);

	*returncode = 0;
	return ctx->result_head;
}

int mod_executesql (void *handle, char *query)
{
//	fprintf (stderr, "ExecuteSql: '%s'\n", query);
	struct modctx *ctx = handle;

	if (ctx == NULL)
		return -4;

	free (ctx->errorstring);
	ctx->errorstring = NULL;
	ctx->nativeerror = 0;

	SQLRETURN ret;

	if (strcmp (query, "COMMIT") == 0) {
		ret = SQLEndTran (SQL_HANDLE_DBC, ctx->hdbc, SQL_COMMIT);

		if (!SQL_SUCCEEDED (ret)) {
//			fprintf (stderr, "SQLEndTran=%d\n", ret);
			return extract_error (ctx->hdbc, SQL_HANDLE_DBC, ctx);
		}

		return 0;
	}

	SQLFreeStmt (ctx->hstmt, SQL_CLOSE);

	const size_t len = utf8_to_ucs2 (ctx, query);

	if (len == 0) {
		ctx->errorstring = strdup ("Error converting the UTF-8 query to UTF-16");
		return -4;
	}

	ret = SQLExecDirectW (ctx->hstmt, (SQLWCHAR *) ctx->string2, len / 2);

	if (ret == SQL_NO_DATA) {
//		fprintf (stderr, "SQLExecDirect=SQL_NO_DATA\n");
		ctx->errorstring = strdup ("No rows matched the WHERE statement");
		return 0;
	}

	if (!SQL_SUCCEEDED (ret)) {
//		fprintf (stderr, "SQLExecDirect=%d\n", ret);
		return extract_error (ctx->hstmt, SQL_HANDLE_STMT, ctx);
	}

	SQLLEN rows;

	ret = SQLRowCount (ctx->hstmt, &rows);

	if (!SQL_SUCCEEDED (ret) || rows == SQL_NULL_DATA) {
//		fprintf (stderr, "SQLRowCount=%d\n", ret);
		rows = 0;
	}

	return rows;
}

char *mod_add_value (void *handle, char *value, size_t maxlen)
{
	struct modctx *ctx = handle;

	if (ctx == NULL || value == NULL)
		return NULL;

	size_t len = strlen (value);

	if (maxlen != 0 && len > maxlen)
		len = string_apply_maxlen (value, len, maxlen);

	const size_t newlen = len * 2;

	if (newlen + 4 > ctx->stringlen ) {
		ctx->stringlen = newlen + 4;
		ctx->stringlen += 1024 - (ctx->stringlen % 1024);

		free (ctx->string);
		ctx->string = malloc (ctx->stringlen);
	}

	char *dst = ctx->string;

	if (ctx->sqlserver == MSSQL)
		*dst++ = 'N';
	else if (ctx->sqlserver == PostgreSQL)
		*dst++ = 'E';

	*dst++ = '\'';

	size_t i;
	if (ctx->sqlserver == MySQL) {
		for (i = 0; i < len; i++) {
			if (value[i] == '\'' || value[i] == '\"' || value[i] == '\\' || value[i] == '%' || value[i] == '_') {
				*dst++ = '\\';
			} else if (value[i] == '\n') {
				*dst++ = '\\';
				*dst++ = 'n';
				continue;
			} else if (value[i] == '\r') {
				*dst++ = '\\';
				*dst++ = 'r';
				continue;
			} else if (value[i] == '\t') {
				*dst++ = '\\';
				*dst++ = 't';
				continue;
			} else if (value[i] == 8) {
				*dst++ = '\\';
				*dst++ = 'b';
				continue;
			} else if (value[i] == 26) {
				*dst++ = '\\';
				*dst++ = 'Z';
				continue;
			}

			*dst++ = value[i];
		}
	} else if (ctx->sqlserver == PostgreSQL) {
		for (i = 0; i < len; i++) {
			if (value[i] == '\'')
				*dst++ = '\'';
			else if (value[i] == '\\')
				*dst++ = '\\';

			*dst++ = value[i];
		}
	} else {
		for (i = 0; i < len; i++) {
			if (value[i] == '\'')
				*dst++ = '\'';

			*dst++ = value[i];
		}
	}

	*dst++ = '\'';
	*dst = '\0';

	return ctx->string;
}

uint8_t	mod_get_status (void *handle)
{
	struct modctx *ctx = handle;

	if (ctx == NULL)
		return 0;

	return ctx->status;
}

void mod_set_status (void *handle, uint8_t newstatus)
{
	struct modctx *ctx = handle;

	if (ctx != NULL)
		ctx->status = newstatus;

	return;
}

uint8_t mod_add_column (void *handle, char *tablename, char *columnname, char *type, char *len, uint8_t nullable, int _unsigned, int command)
{
	struct modctx *ctx = handle;

	if (ctx == NULL)
		return 0;

	size_t maxlen = 128;

	if (command == 0 || command == 1)
		maxlen += strlen (tablename) + strlen (columnname);

	if (ctx->stringlen < maxlen) {
		ctx->stringlen = maxlen;
		free (ctx->string);
		ctx->string = malloc (ctx->stringlen);
	}

	int pos = 0;

	if (command == 0) {
		pos = sprintf (ctx->string, "ALTER TABLE %s ADD %s ", tablename, columnname);
	} else if (command == 1) {
		if (ctx->sqlserver == MySQL)
			pos = sprintf (ctx->string, "ALTER TABLE %s MODIFY COLUMN %s ", tablename, columnname);
		else
			pos = sprintf (ctx->string, "ALTER TABLE %s ALTER COLUMN %s ", tablename, columnname);
	}

	if (ctx->sqlserver == MSSQL) {
		if (strcmp (type, "double") == 0) {
			pos += sprintf (ctx->string + pos, "[float]");
		} else if (strcmp (type, "date") == 0) {
			if (ctx->sqlserver_major >= 10)
				pos += sprintf (ctx->string + pos, "[date]");
			else
				pos += sprintf (ctx->string + pos, "[char](10)");
		} else if (strcmp (type, "time") == 0) {
			if (ctx->sqlserver_major >= 10) {
				pos += sprintf (ctx->string + pos, "[time]");
			} else {
				pos += sprintf (ctx->string + pos, "[char](8)");
				len = NULL;
			}
		} else if (strcmp (type, "longtext") == 0) {
			pos += sprintf (ctx->string + pos, "[nvarchar](max)");
			len = NULL;
		} else if (strcmp (type, "mediumtext") == 0) {
			pos += sprintf (ctx->string + pos, "[nvarchar](max)");
			len = NULL;
		} else if (strcmp (type, "text") == 0) {
			pos += sprintf (ctx->string + pos, "[nvarchar](max)");
			len = NULL;
		} else if (strcmp (type, "varchar") == 0 && command == 0) {
			pos += sprintf (ctx->string + pos, "[nvarchar]");
		} else if (strcmp (type, "bigint") == 0) {
			pos += sprintf (ctx->string + pos, "[numeric]");

			if (len == NULL) {
				pos += sprintf (ctx->string + pos, "(20,0)");
			} else if (strchr (len, ',') == NULL) {
				pos += sprintf (ctx->string + pos, "(%s,0)", len);
				len = NULL;
			}
		} else if (strcmp (type, "decimal") == 0 || strcmp (type, "numeric") == 0) {
			if (len != NULL) {
				char *endp;
				int precision = strtol (len, &endp, 10);
				int scale = 0;

				if (endp != NULL && *endp == ',')
					scale = strtol (endp + 1, NULL, 10);

				/* numeric/decimal on MSSQL have a max size of 38,38 */
				if (precision > 38)
					precision = 38;

				if (scale > 38)
					scale = 38;

				pos += sprintf (ctx->string + pos, "[numeric](%d,%d)", precision, scale);
				len = NULL;
			} else {
				pos += sprintf (ctx->string + pos, "[numeric]");
			}
		} else {
			pos += sprintf (ctx->string + pos, "[%s]", type);
		}
	} else if (ctx->sqlserver == MySQL) {
		const unsigned long ver = ctx->sqlserver_major * 10000 + ctx->sqlserver_minor * 100 + ctx->sqlserver_patch;

		if (strcmp (type, "time") == 0 && ver < 50604) {
			pos += sprintf (ctx->string + pos, "%s", type);
			len = NULL;
		} else {
			pos += sprintf (ctx->string + pos, "%s", type);
		}
	} else if (ctx->sqlserver == PostgreSQL) {
		if (strcmp (type, "mediumtext") == 0) {
			pos += sprintf (ctx->string + pos, "text");
		} else if (strcmp (type, "time") == 0) {
			pos += sprintf (ctx->string + pos, "time without time zone");
		} else if (strcmp (type, "int") == 0) {
			pos += sprintf (ctx->string + pos, "integer");
		} else if (strcmp (type, "double") == 0) {
			pos += sprintf (ctx->string + pos, "double precision");
		} else if (strcmp (type, "char") == 0) {
			pos += sprintf (ctx->string + pos, "character");
		} else if (strcmp (type, "varchar") == 0) {
			pos += sprintf (ctx->string + pos, "character varying");
		} else if (strcmp (type, "bigint") == 0) {
			pos += sprintf (ctx->string + pos, "numeric");

			if (len == NULL) {
				pos += sprintf (ctx->string + pos, "(20,0)");
			} else if (strchr (len, ',') == NULL) {
				pos += sprintf (ctx->string + pos, "(%s,0)", len);
				len = NULL;
			}
		} else {
			pos += sprintf (ctx->string + pos, "%s", type);
		}
	} else {
		pos += sprintf (ctx->string + pos, "%s", type);
	}

	if (len != NULL && 0 != strtoul (len, NULL, 10))
		pos += sprintf (ctx->string + pos, "(%s)", len);

	if (ctx->sqlserver == MySQL && _unsigned != 0)
		pos += sprintf (ctx->string + pos, " UNSIGNED");

	sprintf (ctx->string + pos," %sNULL", nullable == 0 ? "NOT " : "");

	if (command == 0 || command == 1) {
		if (mod_executesql (ctx, ctx->string) >= 0)
			return 1;
	} else if (command == 2) {
		if (tablename != NULL) {
			strcpy (tablename, ctx->string);
			return 1;
		}
	}

	return 0;
}

int mod_get_info (void *handle, mod_info *modinfo, int modinfolen)
{
	struct modctx *ctx = handle;

	if (ctx == NULL)
		return 0;

	ctx->sqlserver = OTHER;
	ctx->sqlserver_major = 0;
	ctx->sqlserver_minor = 0;

	char name[128];
	char ver[128];
	SQLSMALLINT len;

	SQLRETURN ret = SQLGetInfo (ctx->hdbc, SQL_DBMS_NAME, name, sizeof name, &len);

	if (!SQL_SUCCEEDED (ret))
		name[0] = '\0';

	ret = SQLGetInfo (ctx->hdbc, SQL_DBMS_VER, ver, sizeof ver, &len);

	if (!SQL_SUCCEEDED (ret))
		ver[0] = '\0';

	if (name[0] != '\0') {
		if (strcasecmp (name, "MySQL") == 0)
			ctx->sqlserver = MySQL;
		else if (strcasecmp (name, "MariaDB") == 0)
			ctx->sqlserver = MySQL;
		else if (strcasecmp (name, "Microsoft SQL Server") == 0)
			ctx->sqlserver = MSSQL;
		else if (strcasecmp (name, "PostgreSQL") == 0)
			ctx->sqlserver = PostgreSQL;
	}

	if (ver[0] != '\0')
		sscanf (ver, "%d.%d.%d", &ctx->sqlserver_major, &ctx->sqlserver_minor, &ctx->sqlserver_patch);

	if (modinfo == NULL)
		return 1;

	if (modinfolen < (int)sizeof *modinfo)
		return 0;

	if (name[0] == '\0' && ver[0] == '\0') {
		modinfo->version = NULL;
	} else {
		modinfo->version = malloc (strlen (name) + strlen (ver) + 2);

		if (name[0] != '\0')
			sprintf (modinfo->version, "%s %s", name, ver);
		else
			strcpy (modinfo->version, ver);
	}

	modinfo->transaction = NULL;
	modinfo->upsert = 0;

	if (ctx->sqlserver == MySQL)
		modinfo->upsert = 1;
	else if (ctx->sqlserver == PostgreSQL && (ctx->sqlserver_major > 9 || (ctx->sqlserver_major == 9 && ctx->sqlserver_minor >= 5)))
		modinfo->upsert = 2;

	ctx->cfg->version = 0;

	return 1;
}

Databaseinfo *mod_get_databaseinfo (void __attribute__((__unused__)) *handle)
{
	struct modctx *ctx = handle;

	if (ctx == NULL)
		return NULL;

	Databaseinfo *head = NULL;
	Databaseinfo *tail = NULL;

	SQLFreeStmt (ctx->hstmt, SQL_CLOSE);
	SQLRETURN ret = SQLColumns (ctx->hstmt, NULL, 0, NULL, 0, NULL, 0, NULL, 0);

	if (!SQL_SUCCEEDED (ret)) {
		extract_error (ctx->hstmt, SQL_HANDLE_STMT, ctx);
		goto error_exit;
	}

	do {
		ret = SQLFetch (ctx->hstmt);

		if (SQL_SUCCEEDED (ret)) {
			char table[128], column[128], type[128];
			SQLINTEGER maxlen;
			SQLSMALLINT digits, nullable;
			SQLLEN table_len    = SQL_NULL_DATA;
			SQLLEN column_len   = SQL_NULL_DATA;
			SQLLEN type_len     = SQL_NULL_DATA;
			SQLLEN maxlen_len   = SQL_NULL_DATA;
			SQLLEN digits_len   = SQL_NULL_DATA;
			SQLLEN nullable_len = SQL_NULL_DATA;

			SQLGetData (ctx->hstmt,  3, SQL_C_CHAR,   table,     sizeof table,    &table_len);
			SQLGetData (ctx->hstmt,  4, SQL_C_CHAR,   column,    sizeof column,   &column_len);
			SQLGetData (ctx->hstmt,  6, SQL_C_CHAR,   type,      sizeof type,     &type_len);
			SQLGetData (ctx->hstmt,  7, SQL_C_SLONG,  &maxlen,   sizeof maxlen,   &maxlen_len);
			SQLGetData (ctx->hstmt,  9, SQL_C_SSHORT, &digits,   sizeof digits,   &digits_len);
			SQLGetData (ctx->hstmt, 11, SQL_C_SSHORT, &nullable, sizeof nullable, &nullable_len);

			/* make sure that we have data in the columns that we really need here */
			if (table_len == SQL_NULL_DATA || table_len == 0 || column_len == SQL_NULL_DATA || column_len == 0 || type_len == SQL_NULL_DATA || type_len == 0)
				continue;

			Databaseinfo *di = calloc (1, sizeof *di);

			di->tablename = strdup (table);
			di->fieldname = strdup (column);

			/* some SQL servers gives us 'type' in caps */
			char *p;
			for (p = type; *p != '\0'; p++) {
				*p = tolower ((unsigned char)*p);
			}

//			if (strcmp (di->tablename, "instruments") == 0)
//				printf ("%s:%s %s %ld(%ld) %d(%ld) %d %d\n", table, column, type, maxlen, maxlen_len, digits, digits_len, nullable, di->_unsigned);

			if (ctx->sqlserver == MSSQL) {
				if (strcmp (type, "text") == 0) {
					di->type = strdup ("mediumtext");
				} else if (strcmp (type, "ntext") == 0) {
					di->type = strdup ("mediumtext");
				} else if (strcmp (type, "nchar") == 0) {
					di->type = strdup ("char");
				} else if ((strcmp (type, "nvarchar") == 0 || strcmp (type, "varchar") == 0) && (maxlen_len == SQL_NULL_DATA || maxlen == 0 || maxlen > 65535)) {
					di->type = strdup ("mediumtext");
				} else if (strcmp (type, "nvarchar") == 0) {
					di->type = strdup ("varchar");
				} else if (strcmp (type, "numeric") == 0 && maxlen >= 10 && maxlen <= 20 && (digits == 0 || digits_len == SQL_NULL_DATA)) {
					di->type = strdup ("bigint");
					digits_len = SQL_NULL_DATA;
				} else if (strcmp (type, "numeric") == 0) {
					di->type = strdup ("decimal");
				} else if (strcmp (type, "time") == 0) {
					if (digits_len == SQL_NULL_DATA) {
						maxlen = 0;
					} else {
						maxlen = digits;
						digits_len = SQL_NULL_DATA;
					}
				}
			} else if (ctx->sqlserver == MySQL) {
				p = strstr (type, " unsigned");
				if (p != NULL) {
					*p = '\0';
					di->_unsigned = 1;
				}

				if (strcmp (type, "time") == 0) {
					maxlen = 6;
					digits_len = SQL_NULL_DATA;
				} else if (strcmp (type, "decimal") == 0) {
					di->_unsigned = 1;
				} else if (strcmp (type, "bigint") == 0) {
					maxlen_len = SQL_NULL_DATA;
				}
			} else if (ctx->sqlserver == PostgreSQL) {
				if (strcmp (type, "bpchar") == 0) {
					di->type = strdup ("char");
				} else if (strcmp (type, "numeric") == 0 && maxlen <= 20 && (digits == 0 || digits_len == SQL_NULL_DATA)) {
					di->type = strdup ("bigint");
					digits_len = SQL_NULL_DATA;
				} else if (strcmp (type, "numeric") == 0) {
					di->type = strdup ("decimal");
				} else if (strcmp (type, "text") == 0) {
					maxlen_len = SQL_NULL_DATA;
				} else if (strcmp (type, "int4") == 0) {
					di->type = strdup ("int");
				} else if (strcmp (type, "int2") == 0) {
					di->type = strdup ("smallint");
				} else if (strcmp (type, "float8") == 0) {
					di->type = strdup ("double");
					maxlen_len = SQL_NULL_DATA;
				} else if (strcmp (type, "time") == 0) {
					if (digits_len == SQL_NULL_DATA) {
						maxlen = 0;
					} else {
						maxlen = digits;
						digits_len = SQL_NULL_DATA;
					}
				}
			}

			if (di->type == NULL) {
				if (strcmp (type, "float") == 0 || strcmp (type, "double") == 0) {
					di->type = strdup ("double");
					maxlen_len = SQL_NULL_DATA;
				} else if (strcmp (type, "integer") == 0) {
					di->type = strdup ("int");
				} else {
					di->type = strdup (type);
				}
			}

			if (maxlen_len != SQL_NULL_DATA) {
				if (digits_len == SQL_NULL_DATA)
					sprintf (table, "%d", (int)maxlen);
				else
					sprintf (table, "%d,%d", (int)maxlen, digits);

				di->len = strdup (table);
			}

			di->nullable = nullable == SQL_NULLABLE ? 1 : 0;

			if(head == NULL) {
				head = di;
				tail = di;
			} else {
				tail->next = di;
				di->previous = tail;
				tail = di;
			}

		}
	} while (ret != SQL_NO_DATA_FOUND);

	return head;

error_exit:
	mod_free_databaseinfo (head);
	return NULL;
}

void mod_free_databaseinfo (Databaseinfo *dbinfo)
{
	if (dbinfo == NULL)
		return;

	while(dbinfo != NULL) {
		Databaseinfo *next = dbinfo->next;

		free (dbinfo->tablename);
		free (dbinfo->fieldname);
		free (dbinfo->type);
		free (dbinfo->len);
		free (dbinfo);

		dbinfo = next;
	}
}
