#include "mod_postgresql.h"

static char error_message1[] = "Got 0 rows from the SQL-Server. Check that your dbsuser have SELECT rights on the tables in the 'Millistream' database!";

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;
}

int mod_init ()
{
	return 1;
}

int mod_disconnect (void *_handle)
{
	mod_t *handle = _handle;

	if (handle == NULL)
		return -1;

	if (handle->pgcon != NULL)
		PQfinish (handle->pgcon);

	handle->pgcon = NULL;
	return 1;
}

int mod_connect (void *_handle)
{
	mod_t *handle = _handle;

	if (handle == NULL)
		return 0;

	free (handle->lasterrormessage);
	handle->lasterrormessage = NULL;
	handle->inside_transaction = 0;
	handle->last_errorcode = 0;

	if (handle->cfg->connectionstring != NULL) {
		handle->pgcon = PQconnectdb (handle->cfg->connectionstring);
	} else {
		size_t pos = 0;

		/* calculate the needed size of the dynamic connection string */
		if (handle->cfg->host != NULL) pos += strlen (handle->cfg->host) + sizeof ("host='' ");
		if (handle->cfg->port != NULL) pos += strlen (handle->cfg->port) + sizeof ("port='' ");
		if (handle->cfg->database != NULL) pos += strlen (handle->cfg->database) + sizeof ("dbname='' ");
		if (handle->cfg->username != NULL) pos += strlen (handle->cfg->username) + sizeof ("user='' ");
		if (handle->cfg->password != NULL) pos += strlen (handle->cfg->password) + sizeof ("password='' ");
		if (handle->cfg->ssl_key != NULL) pos += strlen (handle->cfg->ssl_key) + sizeof ("sslkey='' ");
		if (handle->cfg->ssl_cert != NULL) pos += strlen (handle->cfg->ssl_cert) + sizeof ("sslcert='' ");
		if (handle->cfg->ssl_ca != NULL) pos += strlen (handle->cfg->ssl_ca) + sizeof ("sslrootcert='' ");

		char connectionstring[pos + 1];
		pos = 0;

		/* build the dynamic connection string */
		if (handle->cfg->host != NULL) pos += sprintf (connectionstring + pos, "host='%s' ", handle->cfg->host);
		if (handle->cfg->port != NULL) pos += sprintf (connectionstring + pos, "port='%s' ", handle->cfg->port);
		if (handle->cfg->database != NULL) pos += sprintf (connectionstring + pos, "dbname='%s' ", handle->cfg->database);
		if (handle->cfg->username != NULL) pos += sprintf (connectionstring + pos, "user='%s' ", handle->cfg->username);
		if (handle->cfg->password != NULL) pos += sprintf (connectionstring + pos, "password='%s' ", handle->cfg->password);
		if (handle->cfg->ssl_key != NULL) pos += sprintf (connectionstring + pos, "sslkey='%s' ", handle->cfg->ssl_key);
		if (handle->cfg->ssl_cert != NULL) pos += sprintf (connectionstring + pos, "sslcert='%s' ", handle->cfg->ssl_cert);
		if (handle->cfg->ssl_ca != NULL) sprintf (connectionstring + pos, "sslrootcert='%s' ", handle->cfg->ssl_ca);

		handle->pgcon = PQconnectdb (connectionstring);
	}

	if (PQstatus (handle->pgcon) != CONNECTION_OK)
	{
		handle->lasterrormessage = strdup (PQerrorMessage (handle->pgcon));
		return 0;
	}

	if (PQsetClientEncoding (handle->pgcon, "UNICODE") != 0 && PQsetClientEncoding (handle->pgcon, "UTF8") != 0) {
		handle->lasterrormessage = strdup (PQerrorMessage (handle->pgcon));
		return 0;
	}

	const char *ret = PQparameterStatus (handle->pgcon, "standard_conforming_strings");

	/* if "standard_conforming_strings" is available and set to "off" then we must prefix escaped strings with 'E' */
	if (ret != NULL && strcmp (ret, "off") == 0)
		handle->use_prefix = 1;

	if (handle->dbname == NULL) {
		char *dbname = PQdb (handle->pgcon);

		if (dbname != NULL)
			handle->dbname = strdup (dbname);
	}

	return 1;
}

int mod_getlasterror (void *_handle)
{
	mod_t *handle = _handle;

	if (handle == NULL)
		return -1;

	return handle->last_errorcode;
}

char *mod_geterrorstring (void *_handle)
{
	mod_t *handle = _handle;

	if (handle == NULL)
		return NULL;

	return (char*)handle->lasterrormessage;
}

void mod_reconnect(void	*_handle)
{
	mod_t *handle = _handle;

	if (handle == NULL)
		return;

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

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

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

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

	return;
}

void mod_freeresult(void *_handle)
{
	mod_t *handle = _handle;

	if (handle == NULL)
		return;

	while (handle->mod_result_head != NULL) {
		mod_result *next = handle->mod_result_head->next;

		free (handle->mod_result_head->columnvalue);
		free (handle->mod_result_head);

		handle->mod_result_head = next;
	}

	handle->mod_result_tail = NULL;
}

void *mod_querysql (void *_handle, char *query, int *returncode)
{
	mod_t *handle = _handle;

	if (handle == NULL)
		return NULL;

	if (handle->pgcon == NULL)
		return NULL;

	int have_savepoint = 0;
	PGresult *res;

	if (handle->inside_transaction == 1) {
		res = PQexec (handle->pgcon, "SAVEPOINT mda_savepoint");

		if (PQresultStatus (res) == PGRES_COMMAND_OK)
			have_savepoint = 1;

		PQclear (res);
	}

	res = PQexec (handle->pgcon, query);
	*returncode = 0;

	if (PQresultStatus (res) != PGRES_TUPLES_OK) {
		if (PQresultStatus (res) ==  PGRES_FATAL_ERROR) {
			free (handle->lasterrormessage);
			handle->lasterrormessage = strdup (PQerrorMessage (handle->pgcon));

			char *errorfield = PQresultErrorField (res, PG_DIAG_SQLSTATE);

			if (have_savepoint == 1) {
				PQclear (res);
				res = PQexec (handle->pgcon, "ROLLBACK TO SAVEPOINT mda_savepoint");
				PQclear (res);
				res = PQexec (handle->pgcon, "RELEASE SAVEPOINT mda_savepoint");
			}

			if (errorfield != NULL) {
				handle->last_errorcode = strtol (errorfield, NULL, 10);

				if (strncmp (errorfield, "08", 2) == 0) { /* Connection Exception */
					PQclear (res);
					*returncode = -2;
					return NULL;
				} else if (strncmp (errorfield, "22", 2) == 0) { /* Data Exception */
					PQclear (res);
					*returncode = -4;
					return NULL;
				} else if (strncmp (errorfield, "23", 2) == 0) { /* Integrity Constraint Violation */
					PQclear (res);
					*returncode = -4;
					return NULL;
				} else if (strncmp (errorfield, "25", 2) == 0) { /* Invalid Transaction State */
					PQclear (res);
					*returncode = -5;
					return NULL;
				} else if (strncmp (errorfield, "40", 2) == 0) { /* Transaction Rollback */
					PQclear (res);
					*returncode = -3;
					return NULL;
				} else if (strncmp (errorfield, "42", 2) == 0) { /* Syntax Error */
					PQclear (res);
					*returncode = -4;
					return NULL;
				}
			}

			PQclear (res);

			if (PQstatus (handle->pgcon) == CONNECTION_OK)
				return NULL;

			*returncode = -4;
			return NULL;
		}
	}

	for (int j = 0 ; j < PQntuples (res); j++) {
		for (int i = 0; i < PQnfields (res);i++) {
			char *field = PQgetvalue (res, j, i);

			if (handle->mod_result_tail == NULL) {
				handle->mod_result_tail = calloc (1, sizeof (mod_result));
				handle->mod_result_head = handle->mod_result_tail;
			} else {
				handle->mod_result_tail->next = calloc (1, sizeof(mod_result));
				handle->mod_result_tail = handle->mod_result_tail->next;
			}

			if (field != NULL && *field != '\0')
				handle->mod_result_tail->columnvalue = strdup (field);
		}
	}

	PQclear (res);

	if (have_savepoint == 1) {
		res = PQexec (handle->pgcon, "RELEASE SAVEPOINT mda_savepoint");
		PQclear (res);
	}

	return handle->mod_result_head;
}

int mod_executesql (void *_handle, char *query)
{
	mod_t *handle = _handle;

	if (handle == NULL)
		return -1;

	if(handle->pgcon == NULL)
		return -1;

	int have_savepoint = 0;
	PGresult *res;

	if (handle->inside_transaction == 1) {
		if (strcmp (query, "ROLLBACK") == 0 || strcmp (query, "COMMIT") == 0) {
			handle->inside_transaction = 0;
		} else {
			res = PQexec (handle->pgcon, "SAVEPOINT mda_savepoint");

			if (PQresultStatus (res) == PGRES_COMMAND_OK)
				have_savepoint = 1;

			PQclear (res);
		}
	} else if (strcmp (query, "START TRANSACTION") == 0) {
		handle->inside_transaction = 1;
	}

	res = PQexec (handle->pgcon, query);

	if (PQresultStatus (res) != PGRES_COMMAND_OK) {
		if (strcmp (query, "START TRANSACTION") == 0)
			handle->inside_transaction = 0;

		if (PQresultStatus (res) == PGRES_FATAL_ERROR) {
			free (handle->lasterrormessage);
			handle->lasterrormessage = strdup (PQerrorMessage (handle->pgcon));

			char *errorfield = PQresultErrorField (res, PG_DIAG_SQLSTATE);

			if (have_savepoint == 1) {
				PQclear (res);
				res = PQexec (handle->pgcon, "ROLLBACK TO SAVEPOINT mda_savepoint");
				PQclear (res);
				res = PQexec (handle->pgcon, "RELEASE SAVEPOINT mda_savepoint");
			}

			if (errorfield != NULL) {
				handle->last_errorcode = strtol (errorfield, NULL, 10);

				if (strncmp (errorfield, "08", 2) == 0) { /* Connection Exception */
					PQclear (res);
					return -2;
				} else if (strncmp (errorfield, "22", 2) == 0) { /* Data Exception */
					PQclear (res);
					return -4;
				} else if (strncmp (errorfield, "23", 2) == 0) { /* Integrity Constraint Violation */
					PQclear (res);
					return -1;
				} else if (strncmp (errorfield, "25", 2) == 0) { /* Invalid Transaction State */
					PQclear (res);
					return -5;
				} else if (strncmp (errorfield, "40", 2) == 0) { /* Transaction Rollback */
					PQclear (res);
					return -3;
				} else if (strncmp (errorfield, "42", 2) == 0) { /* Syntax Error */
					PQclear (res);
					return -4;
				}
			} else {
				handle->last_errorcode = 0;
			}

			PQclear (res);

			if (PQstatus (handle->pgcon) == CONNECTION_OK) {
				if (errorfield == NULL)
					return -1;

				return -4;
			}
			
			return -2;
		}
	}

	const long affectedrows = strtol (PQcmdTuples (res), NULL, 10);

	if (affectedrows == 0) {
		free (handle->lasterrormessage);
		handle->lasterrormessage = strdup ("Affected rows:0");
		handle->last_errorcode = 0;
	}

	PQclear (res);

	if (have_savepoint == 1) {
		res = PQexec (handle->pgcon, "RELEASE SAVEPOINT mda_savepoint");
		PQclear (res);
	}

	return affectedrows;
}

int mod_exit()
{
	return 1;
}

char *mod_add_value (void *_handle, char *value, size_t maxlen)
{
	mod_t *handle = _handle;

	if (handle == NULL || value == NULL || handle->pgcon == NULL)
		return NULL;

	size_t len = strlen (value);

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

	/* PQescapeStringConn() needs (len * 2 + 1) bytes, and then we need to add 3 or 4 bytes depedning upon handle->use_prefix */
	if (len * 2 + 5 > handle->stringlen) {
		handle->stringlen = len * 2 + 5;
		handle->stringlen += 1024 - (handle->stringlen % 1024);
		free (handle->string);
		handle->string = malloc (handle->stringlen);
	}

	if (handle->use_prefix == 0) {
		handle->string[0] ='\'';
		len = PQescapeStringConn (handle->pgcon, handle->string + 1, value, len, NULL);
		handle->string[len+1] = '\'';
		handle->string[len+2] = '\0';
	} else {
		handle->string[0] ='E';
		handle->string[1] ='\'';
		len = PQescapeStringConn (handle->pgcon, handle->string + 2, value, len, NULL);
		handle->string[len + 2] = '\'';
		handle->string[len + 3] = '\0';
	}

	return handle->string;
}

void *mod_create (mod_config *cfg)
{
	void *_handle = calloc (1, sizeof (mod_t));

	if (_handle == NULL)
		return NULL;

	mod_t *handle = _handle;

	handle->string = malloc (1024);
	handle->cfg = cfg;
	handle->stringlen = 1024;

	if (handle->string == NULL) {
		free (_handle);
		return NULL;
	}

	return _handle;
}

int mod_destroy (void *_handle)
{
	mod_t *handle = _handle;

	if (handle == NULL)
		return -1;

	mod_disconnect (handle);
	free (handle->string);
	free (handle);
	return 1;
}

uint8_t	mod_get_status (void *_handle)
{
	mod_t *handle = _handle;

	if (handle == NULL)
		return 0;

	return handle->status;
}

void mod_set_status (void *_handle, uint8_t status)
{
	mod_t *handle = _handle;

	if (handle == NULL)
		return;

	handle->status = status;
	return;
}

Databaseinfo *mod_get_databaseinfo (void *_handle)
{
	mod_t *handle = _handle;

	if (handle == NULL)
		return NULL;

	char *dbname;

	if (handle->dbname != NULL) {
		dbname = handle->dbname;
	} else {
		free (handle->lasterrormessage);
		handle->lasterrormessage = strdup ("Could not determine the name of the current connected database");
		return NULL;
	}

	char query[200 + strlen (dbname)];
	sprintf (query, "SELECT table_name,column_name,data_type,character_maximum_length,numeric_precision,numeric_scale,is_nullable FROM information_schema.columns WHERE table_catalog='%s' AND table_schema='public'", dbname);

	PGresult *res = PQexec (handle->pgcon, query);

	if (PQresultStatus (res) != PGRES_TUPLES_OK) {
		free (handle->lasterrormessage);
		handle->lasterrormessage = strdup (PQerrorMessage (handle->pgcon));
		PQclear (res);
		return NULL;
	}

	const int rows = PQntuples (res);

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

	int i;
	for (i = 0; i < rows; i++) {
		Databaseinfo *di = (Databaseinfo*) calloc (1, sizeof *di);
		di->tablename =  strdup (PQgetvalue (res, i , 0));
		di->fieldname =  strdup (PQgetvalue (res, i, 1));

		const char *val = PQgetvalue (res, i, 2);
		const char *charlen = PQgetvalue (res, i, 3);
		const char *precision = PQgetvalue (res, i, 4);
		const char *scale = PQgetvalue (res, i, 5);

		if (strcmp (val, "time without time zone") == 0)
			di->type = strdup ("time");
		else if (strcmp (val, "text") == 0)
			di->type = strdup ("mediumtext");
		else if (strcmp (val, "character varying") == 0)
			di->type = strdup ("varchar");
		else if (strcmp (val, "character") == 0)
			di->type = strdup ("char");
		else if (strcmp (val, "double precision") == 0)
			di->type = strdup ("double");
		else if (strcmp (val, "numeric") == 0 && strtol (precision, NULL, 10) >= 10 && strtol (precision, NULL, 10) <= 20 && strcmp (scale, "0") == 0)
			di->type = strdup ("bigint");
		else if (strcmp (val, "numeric") == 0)
			di->type = strdup ("decimal");
		else if (strcmp (val, "integer") == 0)
			di->type = strdup ("int");
		else
			di->type = strdup (val);

		if (charlen[0] != '\0') {
			di->len = strdup (charlen);
		} else if (precision[0] != '\0') {
			if (scale[0] != '\0') {
				sprintf (query, "%s,%s", precision, scale);
				di->len = strdup (query);
			} else {
				di->len = strdup (precision);
			}
		}

		if (strcmp (PQgetvalue (res, i, 6), "YES") == 0)
			di->nullable = 1;

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

	PQclear (res);

	if (head == NULL) {
		free (handle->lasterrormessage);
		handle->lasterrormessage = strdup (error_message1);
	}

	return head;
}

uint8_t mod_add_column(void *_handle, char *tablename, char *columnname, char *type, char *len, uint8_t nullable, int __attribute__ ((unused))_unsigned, int command)
{
	mod_t *handle = _handle;
	size_t pos = 0;

	if (handle == NULL)
		return 0;

	if (command == 0)
		pos = sprintf (handle->string ,"ALTER TABLE %s ADD COLUMN %s ", tablename, columnname);
	else if (command == 1)
		pos = sprintf (handle->string ,"ALTER TABLE %s ALTER COLUMN %s TYPE ", tablename, columnname);

	if (type == NULL)
		return 0;

	if (strcmp (type, "bigint") == 0 && len == NULL)
		pos += sprintf (handle->string + pos, "numeric(20,0)");
	else if (strcmp (type, "bigint") == 0 && strchr (len, ',') != NULL)
		pos += sprintf (handle->string + pos, "numeric");
	else if (strcmp (type, "bigint") == 0)
		pos += sprintf (handle->string + pos, "numeric(%s,0)", len), len = NULL;
	else if (strcmp (type, "double") == 0)
		pos += sprintf (handle->string + pos, "double precision");
	else if (strcmp (type, "decimal") == 0)
		pos += sprintf (handle->string + pos, "numeric");
	else if (strcmp (type, "char") == 0)
		pos += sprintf (handle->string + pos, "character");
	else if (strcmp (type, "varchar") == 0)
		pos += sprintf (handle->string + pos, "character varying");
	else if (strcmp (type, "mediumtext") == 0)
		pos += sprintf (handle->string + pos, "text");
	else if (strcmp (type, "time") == 0)
		pos += sprintf (handle->string + pos, "time without time zone"), len = NULL;
	else if (strcmp (type, "datetime") == 0)
		pos += sprintf (handle->string + pos, "time without time zone"), len = NULL;
	else if (strcmp (type, "int") == 0)
		pos += sprintf (handle->string + pos, "integer");
	else
		pos += sprintf (handle->string + pos, "%s",type);

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

	if (command == 0 || command == 2)
		sprintf (handle->string + pos, " %sNULL",nullable == 0 ? "NOT ":"");

	if (command == 0 || command == 1) {
		if (mod_executesql (handle, handle->string) >= 0) {
			if (command == 1) {
				sprintf (handle->string ,"ALTER TABLE %s ALTER COLUMN %s %s", tablename, columnname, nullable == 0 ? "SET NOT NULL" : "DROP NOT NULL");

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

	return 0;
}

int mod_get_info (void *_handle, mod_info *modinfo, int modinfolen)
{
	mod_t *handle = _handle;

	if (handle == NULL || modinfolen < (int)(sizeof (struct mod_info_s)))
		return -1;

	/* PQserverVersion() was introduced in PostgreSQL v8.0.0 */
#if PG_VERSION_NUM >= 80000
	handle->cfg->version = PQserverVersion (handle->pgcon);
#else
	handle->cfg->version = 0;
#endif

	char ver[256];

	if (handle->cfg->version / 10000 < 10)
		sprintf (ver, "%ld.%ld.%ld", handle->cfg->version / 10000, (handle->cfg->version / 100) % 100, handle->cfg->version % 100);
	else
		sprintf (ver, "%ld.%ld", handle->cfg->version / 10000, handle->cfg->version % 10000);

	modinfo->version = strdup (ver);
	modinfo->transaction = strdup ("START TRANSACTION");

	if (handle->cfg->version >= 90500)
		modinfo->upsert = 2;
	else
		modinfo->upsert = 0;

	return 1;
}


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;
	}
}
