//////////////////////////////////////////////////////////////////////////
//
// pgAdmin III - Tab completion
// 
// Copyright (C) 2002 - 2016, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
// tab-complete.cpp - Functions interfacing with tab-complete from psql
//
//////////////////////////////////////////////////////////////////////////

/*
 * BUG: Must compile as C and not C++, because of
 * http://support.microsoft.com/default.aspx?scid=kb%3Ben-us%3B315481
 * Adds a bit of C<->C++ cruft...
 */

#define _CRT_NONSTDC_NO_DEPRECATE
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <libpq-fe.h>
#include <ctype.h>

/*
 * Callbacks to the C++ world
 */
char *pg_query_to_single_ordered_string(char *query, void *dbptr);
int get_id_encoding(void *dbptr);

/*
 * Global vars for readline emulation
 */
static char *rl_line_buffer;

/*
 * Macros to ease typing present in psql, rewritten for our API
 */
#define COMPLETE_WITH_QUERY(query) \
	do { matches = complete_from_query(text,query,NULL,dbptr); } while (0)
#define COMPLETE_WITH_SCHEMA_QUERY(query, addon) \
	do { matches = complete_from_schema_query(text,&query, addon, dbptr); } while (0)
#define COMPLETE_WITH_LIST(list) \
	do { matches = complete_from_list(text,list); } while (0)
#define COMPLETE_WITH_CONST(str) \
	do { matches = complete_from_const(text,str); } while (0)
#define COMPLETE_WITH_ATTR(table) \
	do { matches = complete_from_query(text,Query_for_list_of_attributes, table, dbptr); } while (0)

/*
 * Functions used by the tab completion in psql normally found in pgport
 */
static void *pg_malloc(size_t size)
{
	return malloc(size);
}

static int pg_strcasecmp(const char *s1, const char *s2)
{
#ifdef WIN32
	return _stricmp(s1, s2);
#else
    return strcasecmp(s1, s2);
#endif
}

static int pg_strncasecmp(const char *s1, const char *s2, int len)
{
#ifdef WIN32
	return _strnicmp(s1, s2, len);
#else
    return strncasecmp(s1, s2, len);
#endif
}

/*
 * Return the word (space delimited) before point. Set skip > 0 to
 * skip that many words; e.g. skip=1 finds the word before the
 * previous one. Return value is NULL or a malloc'ed string.
 *
 * Copied directly from psql.
 */
static char *
previous_word(int point, int skip)
{
	int			i,
				start = 0,
				end = -1,
				inquotes = 0;
	char	   *s;

	while (skip-- >= 0)
	{
		/* first we look for a space before the current word */
		for (i = point; i >= 0; i--)
			if (rl_line_buffer[i] == ' ')
				break;

		/* now find the first non-space which then constitutes the end */
		for (; i >= 0; i--)
			if (rl_line_buffer[i] != ' ')
			{
				end = i;
				break;
			}

		/*
		 * If no end found we return null, because there is no word before the
		 * point
		 */
		if (end == -1)
			return NULL;

		/*
		 * Otherwise we now look for the start. The start is either the last
		 * character before any space going backwards from the end, or it's
		 * simply character 0
		 */
		for (start = end; start > 0; start--)
		{
			if (rl_line_buffer[start] == '"')
				inquotes = !inquotes;
			if ((rl_line_buffer[start - 1] == ' ') && inquotes == 0)
				break;
		}

		point = start;
	}

	/* make a copy */
	s = (char *)pg_malloc(end - start + 2);

	strncpy(s, &rl_line_buffer[start], end - start + 1);
	s[end - start + 1] = '\0';

	return s;
}

/* Find the parenthesis after the last word */
/* Copied directly from psql */
static int find_open_parenthesis(int end)
{
	int i = end-1;
	
	while((rl_line_buffer[i]!=' ')&&(i>=0))
	{
		if (rl_line_buffer[i]=='(') return 1;
		i--;
	}
	while((rl_line_buffer[i]==' ')&&(i>=0))
	{
		i--;
	}
	if (rl_line_buffer[i]=='(')
	{
		return 1;       
	}
	return 0;

}


/*
 * Forward declarations
 */
static char *complete_from_list(const char *text, const char * const *list);
static char *complete_from_const(const char *text, const char *string);
static char *complete_from_query(const char *text, const char *query, const char *addon, void *dbptr);
static char *complete_from_schema_query(const char *text, const void* query, const char *addon, void *dbptr);
static char *complete_create_command(char *text);
static char *complete_filename();


/*
 * Include the main tab completion functionality from psql
 */
#include "tab-complete.inc"


/*
 * Completion functions mimicking those from psql, only returning a single space separated
 * string instead of being called multiple times by the readline library.
 */
static char *_complete_from_list(const char *text, const char * const *list, int casesensitive)
{
	 int string_length = strlen(text);
	 int size = 0;
	 int i;
	 char *r;

	 for (i = 0; list[i] != NULL; i++)
	 {
		 if (casesensitive && strncmp(text, list[i], string_length) == 0)
			 size += strlen(list[i]);
		 if (!casesensitive && pg_strncasecmp(text, list[i], string_length) == 0)
			 size += strlen(list[i]);
	 }
	 
	 if (size == 0)
		 return NULL;

	 r = calloc(size + i*2 + 1, 1);
	 for (i = 0; list[i] != NULL; i++)
	 {
		 if ((casesensitive && strncmp(text, list[i], string_length) == 0)
			 ||
			 (!casesensitive && pg_strncasecmp(text, list[i], string_length) == 0))
		 {
			strcat(r,list[i]);
			strcat(r," \t");
		 }
	 }
	 r[strlen(r)-1] = '\0'; /* Chop of trailing space */
	 return r;
}

static char *complete_from_list(const char *text, const char * const *list)
{
	 char *r = _complete_from_list(text, list, 1);
	 if (r)
		 return r;
	 return _complete_from_list(text, list, 0);
}


static char *complete_from_const(const char *text, const char *string)
{
	return strdup(string);
}
static int lower_identifier(const char *ident, char *out,void *dbptr)
{
	size_t		buflen = strlen(ident) + 1;
	
	char	   *sname;
	char	   *oname;
	char	   *optr;
	char	   *tmp;
	int		inquotes=0;
	int isneedlower=0;
    int schemaquoted,objectquoted;
	int encoding=6; // pg_enc.PG_UTF8
	int		enc_is_single_byte = 0;
	int utf8len=0;
	/* Initialize, making a certainly-large-enough output buffer */
	schemaquoted = objectquoted = 0;
	/* Scan */
	inquotes = 0;
	optr=out;
	while (*ident)
	{
		unsigned char ch = (unsigned char) *ident++;
		utf8len++;
		if (ch == '"')
		{
			if (inquotes && *ident == '"')
			{
				/* two quote marks within a quoted identifier = emit quote */
				*optr++ = '"';
				ident++;
				utf8len++;
			}
			else
			{
				inquotes = !inquotes;
				objectquoted = 1;
				*optr++ = '"';
			}
		}
		else if (!enc_is_single_byte && ch>127)
		{
			/*
			 * Transfer multibyte characters without further processing.  They
			 * wouldn't be affected by our downcasing rule anyway, and this
			 * avoids possibly doing the wrong thing in unsafe client
			 * encodings.
			 */
			int			chlen = PQmblenBounded(ident - 1, encoding);

			*optr++ = (char) ch;
			while (--chlen > 0)
				*optr++ = *ident++;
		}
		else
		{
			if (!inquotes)
			{
				/*
				 * This downcasing transformation should match the backend's
				 * downcase_identifier() as best we can.  We do not know the
				 * backend's locale, though, so it's necessarily approximate.
				 * We assume that psql is operating in the same locale and
				 * encoding as the backend.
				 */
				if (ch >= 'A' && ch <= 'Z')
					ch += 'a' - 'A';
				else if (enc_is_single_byte && ch>127 && isupper(ch))
					ch = tolower(ch);
			}
			*optr++ = (char) ch;
		}
	}
	*optr = '\0';
	return utf8len;
}

static char* _complete_from_query(const char* text, const char* query, const SchemaQuery* squery, const char* addon, void* dbptr)
{
	int string_length = strlen(text);
	char* e_text;
	char* e_text_ident;
	char* complete_query = NULL;
	char* t;

	e_text_ident = malloc(string_length * 2 + 1);
	PQescapeString(e_text_ident,text, strlen(text));
	string_length=strlen(e_text_ident);
	e_text = malloc(string_length * 2 + 1);
	int utf8len=lower_identifier(e_text_ident,e_text,dbptr);
	string_length=utf8len;
	free(e_text_ident);

	if (query != NULL)
	{
		/* Normal query */
		int bufsize = 1024;
		char* e_addon;

		/* Normal query needs escaped string */
		if (addon)
		{
			e_addon = malloc(strlen(addon) * 2 + 1);
			PQescapeString(e_addon, addon, strlen(addon));
		}
		else
			e_addon = strdup("");

		while (1)
		{
			int r;
			complete_query = realloc(complete_query, bufsize);
#ifdef WIN32
			r = _snprintf(complete_query, bufsize, query, string_length, e_text, e_addon);
#else
			r = snprintf(complete_query, bufsize, query, string_length, e_text, e_addon);
#endif
			if (r < 0 || r >= bufsize)
				bufsize *= 2;
			else
				break;
		}
		free(e_addon);
	}
	else
	{
		/* Schema query */
		char* selcondition = NULL;
		char* viscondition = NULL;
		char* suppress_str = NULL;
		const char* qualresult;
		int bufsize = 2048;

		if (squery->selcondition)
		{
			selcondition = malloc(strlen(squery->selcondition) + 10);
			sprintf(selcondition, "%s AND ", squery->selcondition);
		}
		else
			selcondition = strdup("");

		if (squery->viscondition)
		{
			viscondition = malloc(strlen(squery->viscondition) + 10);
			sprintf(viscondition, " AND %s", squery->viscondition);
		}
		else
			viscondition = strdup("");

		if (strcmp(squery->catname, "pg_catalog.pg_class c") == 0 &&
			strncmp(text, "pg_", 3) != 0)
			suppress_str = " AND c.relnamespace <> (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = 'pg_catalog')";
		else
			suppress_str = "";

		qualresult = squery->qualresult;
		if (qualresult == NULL)
			qualresult = squery->result;

		while (1)
		{
			int r;
			complete_query = realloc(complete_query, bufsize);
#ifdef WIN32
			r = _snprintf(complete_query, bufsize,
#else
			r = snprintf(complete_query, bufsize,
#endif
				"SELECT %s FROM %s WHERE %s substring(%s,1,%d)='%s' %s %s "
				"\nUNION\n"
				"SELECT pg_catalog.quote_ident(n.nspname) || '.' FROM pg_catalog.pg_namespace n WHERE pg_catalog.has_schema_privilege(n.oid,'usage') and substring(pg_catalog.quote_ident(n.nspname) || '.',1,%d)='%s' AND (SELECT pg_catalog.count(*) FROM pg_catalog.pg_namespace WHERE substring(pg_catalog.quote_ident(nspname) || '.',1,%d)= substring('%s',1,pg_catalog.length(pg_catalog.quote_ident(nspname))+1))>1"
				"\nUNION\n"
				"SELECT pg_catalog.quote_ident(n.nspname) || '.' || %s FROM %s, pg_catalog.pg_namespace n WHERE pg_catalog.has_schema_privilege(n.oid,'usage') and %s = n.oid AND %s substring(pg_catalog.quote_ident(n.nspname) || '.' || %s,1,%d)='%s' AND substring(pg_catalog.quote_ident(n.nspname) || '.',1,%d) = substring('%s',1,pg_catalog.length(pg_catalog.quote_ident(n.nspname))+1) AND (SELECT pg_catalog.count(*) FROM pg_catalog.pg_namespace WHERE substring(pg_catalog.quote_ident(nspname) || '.',1,%d) = substring('%s',1,pg_catalog.length(pg_catalog.quote_ident(nspname))+1)) = 1"
				"\n%s",
				squery->result,
				squery->catname,
				selcondition,
				squery->result,
				string_length,
				e_text,
				viscondition,
				suppress_str,
				string_length,
				e_text,
				string_length,
				e_text,
				qualresult,
				squery->catname,
				squery->namespace,
				selcondition,
				qualresult,
				string_length,
				e_text,
				string_length,
				e_text,
				string_length,
				e_text,
				addon ? addon : "");

			if (r < 0 || r >= bufsize)
				bufsize *= 2;
			else
			{
				// namespace=='-'
				if (squery->namespace[0] == '-') {
					char* newlinePtr = strchr(complete_query, '\n');
					if (newlinePtr != NULL) *newlinePtr = '\0';

				}
			}
			break;
		}
		free(viscondition);
		free(selcondition);

	}
	t = pg_query_to_single_ordered_string(complete_query, dbptr);
	if (complete_query)
		free(complete_query);
	free(e_text);
	return t;
}

static char *complete_from_query(const char *text, const char *query, const char *addon, void *dbptr)
{
	return _complete_from_query(text, query, NULL, addon, dbptr);
}

static char *complete_from_schema_query(const char *text, const void* query, const char *addon, void *dbptr)
{
	/* query really is a SchemaQuery, but we don't know about SchemaQuery early enough. */
	return _complete_from_query(text, NULL, (SchemaQuery *)query, addon, dbptr);
}

static char *complete_create_command(char *text)
{
	 int string_length = strlen(text);
	 int size = 0;
	 int i;
	 char *r;

	 for (i = 0; words_after_create[i].name != NULL; i++)
	 {
		 if (pg_strncasecmp(text, words_after_create[i].name, string_length) == 0)
			 size += strlen(words_after_create[i].name);
	 }
	 if (size == 0)
		 return NULL;

	 r = calloc(size + i*2 + 1, 1);
	 for (i = 0; words_after_create[i].name != NULL; i++)
	 {
		 if (pg_strncasecmp(text, words_after_create[i].name, string_length) == 0)
		 {
			strcat(r,words_after_create[i].name);
			strcat(r," \t");
		 }
	 }
		r[strlen(r)-1] = '\0'; /* Chop of trailing space */
	 return r;
}

/* Not implemented */
static char *complete_filename()
{
	return NULL;
}


/*
 * Entrypoint from the C++ world
 */
char *tab_complete(const char *allstr, const int startptr, const int endptr, void *dbptr)
{
	rl_line_buffer = (char *)allstr;
	return psql_completion((char *)(allstr + startptr), startptr,endptr,dbptr);
}
