/***************************************************************************

	main.c

	(c) 2000-2017 Benoît Minisini <benoit.minisini@gambas-basic.org>

	This program is free software; you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation; either version 2, or (at your option)
	any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
	MA 02110-1301, USA.

***************************************************************************/

#define __MAIN_C

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <ctype.h>
#include <sys/time.h>

#include "gb_common.h"

#include "c_subcollection.h"
#include "CConnection.h"
#include "CDatabase.h"
#include "CUser.h"
#include "CTable.h"
#include "CField.h"
#include "CIndex.h"
#include "CResult.h"
#include "CResultField.h"

#include "sqlite.h"
#include "main.h"


GB_INTERFACE GB EXPORT;


DB_DATABASE *DB_CurrentDatabase = NULL;

static DB_DRIVER *_drivers[MAX_DRIVER];
static int _drivers_count = 0;
static char *_query = NULL;

#define TEMP_MAX 64
static char _temp[TEMP_MAX];
static int _temp_len;

static bool _debug = FALSE;
static const char *_try_another = NULL;

static GB_COLLECTION _options;


int DB_CheckNameWith(const char *name, const char *msg, const char *more)
{
	unsigned char c;
	const char *p = name;

	if (!name || !*name)
	{
		GB.Error("Void &1 name", msg);
		return TRUE;
	}

	while ((c = *p++))
	{
		if ((c >= 'A' && c <='Z') || (c >= 'a' && c <='z') || (c >= '0' && c <= '9') || c == '_')
			continue;

		if (more && index(more, c))
			continue;

		GB.Error("Bad &1 name: &2", msg, name);
		return TRUE;
	}

	return FALSE;
}


void DB_FreeStringArray(char ***parray)
{
	int i;
	char **array = *parray;

	if (!*parray)
		return;

	for (i = 0; i < GB.Count(array); i++)
		GB.FreeString(&array[i]);

	GB.FreeArray(parray);
}

GB_ARRAY DB_StringArrayToGambasArray(char **array)
{
	GB_ARRAY garray;
	int i, n;
	char *str;

	n = GB.Count(array);

	GB.Array.New(&garray, GB_T_STRING, n);

	for (i = 0; i < n; i++)
	{
		str = GB.NewZeroString(array[i]);
		*((char **)GB.Array.Get(garray, i)) = str;
	}

	return garray;
}

int DB_FindStringArray(char **array, const char *elt)
{
	int i;

	for (i = 0; i < GB.Count(array); i++)
	{
		if (!strcasecmp(elt, array[i]))
			return i;
	}

	return -1;
}


static void DB_Register(DB_DRIVER *driver)
{
	if (_drivers_count >= MAX_DRIVER)
		return;

	_drivers[_drivers_count] = driver;
	_drivers_count++;
}


void DB_TryAnother(const char *driver)
{
	_try_another = driver;
}

static DB_DRIVER *DB_GetDriver(const char *type)
{
	int i;
	char comp[type ? strlen(type) + 8 : 1];

	if (!type)
	{
		GB.Error("Driver name missing");
		return NULL;
	}

	strcpy(comp, "gb.db.");
	strcat(comp, type);

	GB.Component.Load(comp);
	GB.Error(NULL); // reset the error flag;
	
	for (i = 0; i < _drivers_count; i++)
	{
		if (strcasecmp(_drivers[i]->name, type) == 0)
			return _drivers[i];
	}

	GB.Error("Cannot find driver for database: &1", type);
	return NULL;
}


bool DB_Open(DB_DESC *desc, DB_DRIVER **driver, DB_DATABASE *db, GB_COLLECTION options)
{
	DB_DRIVER *d;
	int res;
	int timeout;
	const char *type = desc->type;

	_options = options;
	
	timeout = db->timeout;
	CLEAR(db);
	db->timeout = timeout;

	for(;;)
	{
		d = DB_GetDriver(type);
		if (!d)
			return TRUE;

		*driver = d;

		_try_another = NULL;
		res = (*d->Open)(desc, db);
		if (!res)
			return FALSE;

		if (!_try_another)
			return TRUE;

		type = _try_another;
	}
}


void DB_GetOptions(DB_OPTIONS_CALLBACK cb)
{
	GB_COLLECTION_ITER iter;
	GB_VALUE value;
	char *key;
	int len;
	
	GB.Collection.Enum(_options, &iter, NULL, NULL, NULL);
	for(;;)
	{
		if (GB.Collection.Enum(_options, &iter, (GB_VARIANT *)&value, &key, &len))
			break;
		
		GB.BorrowValue(&value);
		(*cb)(GB.TempString(key, len), &value);
		GB.ReleaseValue(&value);
	}
}


void DB_Format(DB_DRIVER *driver, GB_VALUE *arg, DB_FORMAT_CALLBACK add)
{
	static char buffer[32];

	char *s;
	int l;
	int i;

	if (arg->type == GB_T_VARIANT)
		GB.Conv(arg, ((GB_VARIANT *)arg)->value.type);

	if (arg->type == (GB_TYPE)CLASS_Blob)
	{
		(*driver->FormatBlob)((DB_BLOB *)(((GB_OBJECT *)arg)->value), add);
		return;
	}

	if ((arg->type == GB_T_DATE && arg->_date.value.date == 0 && arg->_date.value.time == 0)
			|| (arg->type == GB_T_STRING && arg->_string.value.len == 0)
			|| (arg->type == GB_T_NULL))
	{
		add("NULL", 4);
		return;
	}
	
	if (!(*driver->Format)(arg, add))
	{
		switch (arg->type)
		{
			case GB_T_BOOLEAN:

				if (VALUE((GB_BOOLEAN *)arg))
					add("TRUE", 4);
				else
					add("FALSE", 5);

				return;

			case GB_T_BYTE:
			case GB_T_SHORT:
			case GB_T_INTEGER:

				l = sprintf(buffer, "%d", VALUE((GB_INTEGER *)arg));
				add(buffer, l);
				return;

			case GB_T_LONG:

				l = sprintf(buffer, "%" PRId64, VALUE((GB_LONG *)arg));
				add(buffer, l);
				return;

			case GB_T_FLOAT:
				
				GB.NumberToString(FALSE, VALUE((GB_FLOAT *)arg), NULL, &s, &l);
				add(s, l);

				return;

			case GB_T_STRING:
			case GB_T_CSTRING:

				s = VALUE((GB_STRING *)arg).addr + VALUE((GB_STRING *)arg).start;
				l = VALUE((GB_STRING *)arg).len;

				add("'", 1);

				for (i = 0; i < l; i++, s++)
				{
					add(s, 1);
					if (*s == '\'' || *s == '\\')
						add(s, 1);
				}

				add("'", 1);
				return;

			default:
				fprintf(stderr, "gb.db: DB_Format: unsupported datatype: %d\n", (int)arg->type);
				return;
		}
	}
}


void DB_FormatVariant(DB_DRIVER *driver, GB_VARIANT_VALUE *arg, DB_FORMAT_CALLBACK add)
{
	GB_VALUE value;

	value.type = arg->type;

	switch(arg->type)
	{
		case GB_T_NULL:
			break;

		case GB_T_STRING:
		case GB_T_CSTRING:
			{
				GB_STRING *val = (GB_STRING *)(void *)&value;
				val->value.addr = arg->value._string;
				val->value.start = 0;
				if (arg->type == GB_T_STRING)
					val->value.len = GB.StringLength(arg->value._string);
				else
					val->value.len = strlen(arg->value._string);
			}
			break;

		default:
			value.type = GB_T_VARIANT;
			value._variant.value = *arg;
			GB.Conv(&value, arg->type);
			break;
	}

	DB_Format(driver, &value, add);
}


static int query_narg;
static GB_VALUE *query_arg;
static DB_DRIVER *query_driver;
static DB_DATABASE *query_db;

static void mq_add_param(int index, char before, char after)
{
	GB_VALUE *arg;

	if (index < 1 || index > query_narg)
		return;

	arg = &query_arg[index - 1];

	//fprintf(stderr, "mq_add_param: %d %c %c\n", index, before, after);

	if (before == '[' && after == ']')
	{
		GB.SubstStringUnquote();
		if (!GB.Conv(arg, GB_T_STRING))
			GB.SubstAddCallback(DB_GetQuotedTable(query_driver, query_db, arg->_string.value.addr + arg->_string.value.start, arg->_string.value.len), -1);
	}
	else if ((before == '\'' || before == '`') && after == before)
	{
		GB.SubstStringUnquote();
		if (!GB.Conv(arg, GB_T_STRING))
		{
			GB.SubstAddCallback(query_driver->GetQuote(), -1);
			GB.SubstAddCallback(arg->_string.value.addr + arg->_string.value.start, arg->_string.value.len);
			GB.SubstAddCallback(query_driver->GetQuote(), -1);
		}
	}
	else
		DB_Format(query_driver, arg, (DB_FORMAT_CALLBACK)GB.SubstAddCallback);
}

char *DB_MakeQuery(DB_DRIVER *driver, DB_DATABASE *db, const char *pattern, int len, int narg, GB_VALUE *arg)
{
	char *query;

	query_narg = narg;
	query_arg = arg;
	query_driver = driver;
	query_db = db;

	if (narg == 0)
		query = GB.TempString(pattern, len);
	else
		query = GB.SubstStringAdd(pattern, len, mq_add_param);

	if (!query || *query == 0)
	{
		GB.Error("Void query");
		return NULL;
	}
	else
		return query;
}


void q_init(void)
{
	GB.FreeString(&_query);
	_query = NULL;
	_temp_len = 0;
}

static void q_dump_temp(void)
{
	if (!_temp_len)
		return;
		
	_query = GB.AddString(_query, _temp, _temp_len);
	_temp_len = 0;
}

void q_add_length(const char *str, int len)
{
	if (!str)
		return;
	
	if ((_temp_len + len) > TEMP_MAX)
		q_dump_temp();
	
	if (len > TEMP_MAX)
		_query = GB.AddString(_query, str, len);
	else
	{
		memcpy(&_temp[_temp_len], str, len);
		_temp_len += len;
	}
}

void q_add(const char *str)
{
	if (str)
		q_add_length(str, strlen(str));
}

void q_add_lower(const char *str)
{
	int i, len;
	char *lstr;

	if (!str)
		return;

	len = strlen(str);

	if (len <= 0)
		return;

	lstr = GB.TempString(str, len);
	for (i = 0; i < len; i++)
		lstr[i] = GB.ToLower(lstr[i]);

	q_add_length(lstr, len);
}

char *q_get(void)
{
	q_dump_temp();
	return _query;
}

char *q_steal(void)
{
	char *s;
	q_dump_temp();
	s = _query;
	_query = NULL;
	return s;
}

int q_length(void)
{
	return GB.StringLength(_query) + _temp_len;
}

void DB_SetDebug(int debug)
{
	_debug = debug;
}

int DB_IsDebug(void)
{
	return _debug;
}

void DB_Debug(const char *prefix, const char *msg, ...)
{
	va_list args;
	struct timeval tv;
	GB_DATE_SERIAL *date;
	GB_DATE val;

	if (!_debug)
		return;

	if (gettimeofday(&tv, NULL) == 0)
	{
		GB.MakeDateFromTime((time_t)tv.tv_sec, tv.tv_usec, &val);
		date = GB.SplitDate(&val);
		fprintf(stderr, "%04d-%02d-%02d %02d:%02d:%02d.%03d ", date->year, date->month, date->day, date->hour, date->min, date->sec, date->msec);
	}
	
	fprintf(stderr, "%s: ", prefix);
	
	va_start(args, msg);
	vfprintf(stderr, msg, args);
	va_end(args);
	
	fputc('\n', stderr);
	fflush(stderr);
}


static char *_quote;
DB_SUBST_CALLBACK _quote_cb;

static void ss_get_param(int index, char **str, int *len)
{
	(*_quote_cb)(index, str, len, _quote[index]);
}

char *DB_SubstString(const char *pattern, int len_pattern, DB_SUBST_CALLBACK add)
{
	int i;
	unsigned char last_c, c;
	int n;
	char quote[20] = {0};
	
	c = 0;
	
	len_pattern--;
	for (i = 0; i < len_pattern; i++)
	{
		last_c = c;
		c = pattern[i];
		
		if (c == '&')
		{
			c = pattern[++i];
			if (c == '&')
				continue;
			if (isdigit(c))
			{
				n = c - '0';
				c = pattern[++i];
				if (isdigit(c))
				{
					n = n * 10 + c - '0';
					i++;
				}
				quote[n] = last_c;
			}
		} 
	}
	
	_quote_cb = add;
	_quote = quote;
	
	return GB.SubstString(pattern, len_pattern, ss_get_param);
}

char *DB_QuoteString(const char *str, int len, char quote)
{
	char *res, *p, c;
	int len_res;
	int i;
	
	len_res = len;
	for (i = 0; i < len; i++)
	{
		if (str[i] == quote)
			len_res++;
	}
	
	res = GB.TempString(NULL, len_res);
	
	p = res;
	for (i = 0; i < len; i++)
	{
		c = str[i];
		*p++ = c;
		if (c == quote || c == '\\')
			*p++ = c;
	}
	*p = 0;
	
	return res;
}

char *DB_UnquoteString(const char *str, int len, char quote)
{
	char *res, *p, c;
	int len_res;
	int i;
	
	if (len >= 2 && str[0] == quote && str[len - 1] == quote)
	{
		str++;
		len -= 2;
	}
	
	if (!len)
		return "";
	
	len_res = len;
	for (i = 0; i < (len - 1); i++)
	{
		if ((str[i] == quote && str[i + 1] == quote) || str[i] == '\\')
		{
			len_res--;
			i++;
		}
	}
	
	res = GB.TempString(NULL, len_res);
	
	p = res;
	for (i = 0; i < len; i++)
	{
		c = str[i];
		if (c == quote && (i + 1) < len && str[i + 1] == quote)
			i++;
		else if (c == '\\' && (i + 1) < len)
		{
			c = str[i + 1];
			i++;
		}

		*p++ = c;
	}
	*p = 0;
	
	return res;
}

DB_DATABASE *DB_GetCurrent()
{
	return DB_CurrentDatabase;
}

char *DB_GetQuotedTable(DB_DRIVER *driver, DB_DATABASE *db, const char *table, int len)
{
	char *point = NULL;
	char *res;
	const char *quote;
	
	if (!table)
		return "";
	
	if (len < 0)
		len = strlen(table);
	
	if (len == 0)
		return "";
	
	if (db->flags.schema)
		point = index(table, '.');
	
	quote = (*driver->GetQuote)();
	
	if (!point)
	{
		res = GB.TempString(NULL, len + 2);
		sprintf(res, "%s%.*s%s", quote, len, table, quote);
	}
	else
	{
		int len_schema = (int)(point - table);
		res = GB.TempString(NULL, len + 4);
		sprintf(res, "%s%.*s%s.%s%.*s%s", quote, len_schema, table, quote, quote, len - len_schema - 1, point + 1, quote);
	}
	
	return res;
}


GB_DESC *GB_CLASSES [] EXPORT =
{
	SubCollectionDesc,
	CIndexDesc,
	CFieldDesc,
	CTableFieldsDesc,
	CTableIndexesDesc,
	CTableDesc,
	CUserDesc,
	CDatabaseDesc,
	CConnectionUsersDesc,
	CConnectionDatabasesDesc,
	CConnectionTablesDesc,
	CConnectionDesc,
	CDBDesc,
	CBlobDesc,
	CResultFieldDesc,
	CResultFieldsDesc,
	CResultDesc,
	NULL
};

void *GB_DB_1[] EXPORT = {

	(void *)1,

	(void *)DB_Register,
	(void *)DB_Format,
	(void *)DB_FormatVariant,
	(void *)DB_IsDebug,
	(void *)DB_Debug,
	(void *)DB_TryAnother,
	(void *)DB_SubstString,
	(void *)DB_QuoteString,
	(void *)DB_UnquoteString,
	(void *)DB_GetCurrent,
	(void *)DB_GetOptions,

	(void *)q_init,
	(void *)q_add,
	(void *)q_add_lower,
	(void *)q_add_length,
	(void *)q_get,
	(void *)q_steal,
	(void *)q_length,

	(void *)DB_FindStringArray,

	NULL
};


int EXPORT GB_INIT(void)
{
	char *env = getenv("GB_DB_DEBUG");

	if (env && strcmp(env, "0"))
		DB_SetDebug(TRUE);

	DB_Register(&DB_sqlite_pseudo_driver);
	return 0;
}


void EXPORT GB_EXIT()
{
	GB.FreeString(&_query);
}

