/* MDB Tools - A library for reading MS Access database file
 * Copyright (C) 2000 Brian Bruns
 *
 * 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 of the License, 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.
 */

#include <stdio.h>

#ifdef HAVE_LIBREADLINE
#  if defined(HAVE_READLINE_READLINE_H)
#    include <readline/readline.h>
#  elif defined(HAVE_READLINE_H)
#    include <readline.h>
#  else
/* no readline.h */
extern char *readline ();
#  endif
char *cmdline = NULL;
#endif /* HAVE_LIBREADLINE */

#ifdef HAVE_READLINE_HISTORY
#  if defined(HAVE_READLINE_HISTORY_H)
#    include <readline/history.h>
#  elif defined(HAVE_HISTORY_H)
#    include <history.h>
#  else
/* no history.h */
extern void add_history ();
extern int write_history ();
extern int read_history ();
extern void clear_history ();
#  endif
#endif /* HAVE_READLINE_HISTORY */

#include <string.h>
#include "mdbsql.h"
#include "mdbver.h"

void dump_results(FILE *out, MdbSQL *sql, char *delimiter);
void dump_results_pp(FILE *out, MdbSQL *sql);

int headers = 1;
int footers = 1;
int pretty_print = 1;
int showplan = 0;
int noexec = 0;

#ifdef HAVE_READLINE_HISTORY
#define HISTFILE ".mdbhistory"
#endif

#ifndef HAVE_LIBREADLINE
char *readline(char *prompt)
{
char line[1000];
int i = 0;

	fputs(prompt, stdout);
	if (!fgets(line, sizeof(line), stdin)) {
		return NULL;
	}
	for (i=strlen(line);i>0;i--) {
		if (line[i]=='\n') {
			line[i]='\0';
			break;
		}
	}

	return g_strdup(line);
}
#endif

static int strlen_utf(const char *s) {
	int len = 0;
	while (*s) {
		if ((*s++ & 0xc0) != 0x80)
			len++;
	}
	return len;
}

void
do_set_cmd(MdbSQL *sql, char *s)
{
	char *level1, *level2;
	level1 = strtok(s, " \t\n");
	if (!level1) {
		printf("Usage: set [stats|showplan|noexec] [on|off]\n");
		return;
	}
	if (!strcmp(level1,"stats")) {
		level2 = strtok(NULL, " \t");
		if (!level2) {
			printf("Usage: set stats [on|off]\n");
			return;
		}
		if (!strcmp(level2,"on")) {
			mdb_stats_on(sql->mdb);
		} else if (!strcmp(level2,"off")) {
			mdb_stats_off(sql->mdb);
			mdb_dump_stats(sql->mdb);
		} else {
			printf("Unknown stats option %s\n", level2);
			printf("Usage: set stats [on|off]\n");
		}
	} else if (!strcmp(level1,"showplan")) {
		level2 = strtok(NULL, " \t");
		if (!level2) {
			printf("Usage: set showplan [on|off]\n");
			return;
		}
		if (!strcmp(level2,"on")) {
			showplan=1;
		} else if (!strcmp(level2,"off")) {
			showplan=0;
		} else {
			printf("Unknown showplan option %s\n", level2);
			printf("Usage: set showplan [on|off]\n");
		}
	} else if (!strcmp(level1,"noexec")) {
		level2 = strtok(NULL, " \t");
		if (!level2) {
			printf("Usage: set noexec [on|off]\n");
			return;
		}
		if (!strcmp(level2,"on")) {
			noexec=1;
		} else if (!strcmp(level2,"off")) {
			noexec=0;
		} else {
			printf("Unknown noexec option %s\n", level2);
			printf("Usage: set noexec [on|off]\n");
		}
	} else {
		printf("Unknown set command %s\n", level1);
		printf("Usage: set [stats|showplan|noexec] [on|off]\n");
	}
}

void 
run_query(FILE *out, MdbSQL *sql, char *mybuf, char *delimiter)
{
	MdbTableDef *table;

	mdb_sql_run_query(sql, mybuf);
	if (!mdb_sql_has_error(sql)) {
		if (showplan) {
			table = sql->cur_table;
			if (table->sarg_tree) mdb_sql_dump_node(table->sarg_tree, 0);
			if (sql->cur_table->strategy == MDB_TABLE_SCAN)
				printf("Table scanning %s\n", table->name);
			else 
				printf("Index scanning %s using %s\n", table->name, table->scan_idx->name);
		}
		/* If noexec != on, dump results */
		if (!noexec) {
			if (pretty_print)
				dump_results_pp(out, sql);
			else
				dump_results(out, sql, delimiter);
		}
		mdb_sql_reset(sql);
	}
}

void print_value(FILE *out, char *v, int sz, int first)
{
int i;
int vlen;

	if (first)
		fputc('|', out);
	vlen = strlen_utf(v);
	fputs(v, out);
	for (i=vlen;i<sz;i++)
		fputc(' ', out);
	fputc('|', out);
}
static void print_break(FILE *out, int sz, int first)
{
int i;
	if (first)
		fputc('+', out);
	for (i=0;i<sz;i++)
		fputc('-', out);
	fputc('+', out);
}
void print_rows_retrieved(FILE *out, unsigned long row_count)
{
	if (!row_count) 
		fprintf(out, "No Rows retrieved\n");
	else if (row_count==1)
		fprintf(out, "1 Row retrieved\n");
	else 
		fprintf(out, "%lu Rows retrieved\n", row_count);
	fflush(out);
}
void
dump_results(FILE *out, MdbSQL *sql, char *delimiter)
{
	unsigned int j;
	MdbSQLColumn *sqlcol;

	if (headers) {
		for (j=0;j<sql->num_columns-1;j++) {
			sqlcol = g_ptr_array_index(sql->columns,j);
			fprintf(out, "%s%s", sqlcol->name,
				delimiter ? delimiter : "\t");
		}
		sqlcol = g_ptr_array_index(sql->columns,sql->num_columns-1);
		fprintf(out, "%s", sqlcol->name);
		fprintf(out,"\n");
		fflush(out);
	}
	while(mdb_sql_fetch_row(sql, sql->cur_table)) {
  		for (j=0;j<sql->num_columns-1;j++) {
			sqlcol = g_ptr_array_index(sql->columns,j);
			fprintf(out, "%s%s", (char*)(g_ptr_array_index(sql->bound_values, j)),
				delimiter ? delimiter : "\t");
		}
		sqlcol = g_ptr_array_index(sql->columns,sql->num_columns-1);
		fprintf(out, "%s", (char*)(g_ptr_array_index(sql->bound_values, sql->num_columns-1)));
		fprintf(out,"\n");
		fflush(out);
	}
	if (footers) {
		print_rows_retrieved(out, sql->row_count);
	}
}

void 
dump_results_pp(FILE *out, MdbSQL *sql)
{
	unsigned int j;
	MdbSQLColumn *sqlcol;

	/* print header */
	if (headers) {
		for (j=0;j<sql->num_columns;j++) {
			sqlcol = g_ptr_array_index(sql->columns,j);
			if (strlen(sqlcol->name)>(size_t)sqlcol->disp_size)
				sqlcol->disp_size = strlen(sqlcol->name);
			print_break(out, sqlcol->disp_size, !j);
		}
		fprintf(out,"\n");
		fflush(out);
		for (j=0;j<sql->num_columns;j++) {
			sqlcol = g_ptr_array_index(sql->columns,j);
			print_value(out, sqlcol->name,sqlcol->disp_size,!j);
		}
		fprintf(out,"\n");
		fflush(out);
	}

	for (j=0;j<sql->num_columns;j++) {
		sqlcol = g_ptr_array_index(sql->columns,j);
		print_break(out, sqlcol->disp_size, !j);
	}
	fprintf(out,"\n");
	fflush(out);

	/* print each row */
	while(mdb_sql_fetch_row(sql, sql->cur_table)) {
  		for (j=0;j<sql->num_columns;j++) {
			sqlcol = g_ptr_array_index(sql->columns,j);
			print_value(out, (char *) g_ptr_array_index(sql->bound_values, j), sqlcol->disp_size,!j);
		}
		fprintf(out,"\n");
		fflush(out);
	}

	/* footer */
	for (j=0;j<sql->num_columns;j++) {
		sqlcol = g_ptr_array_index(sql->columns,j);
		print_break(out, sqlcol->disp_size, !j);
	}
	fprintf(out,"\n");
	fflush(out);
	if (footers) {
		print_rows_retrieved(out, sql->row_count);
	}
}

static char *
find_sql_terminator(char *s)
{
	char *sp;
	int len = strlen(s);

	if (len == 0) {
		return NULL;
	}

	sp = &s[len-1];
	while (sp > s && isspace((int)*sp)) {
		sp--;
	}

	if (*sp == ';') {
		return sp;
	}

	return NULL;
}

int
main(int argc, char **argv)
{
	char *s = NULL;
	char prompt[20];
	int line = 0;
	char *mybuf;
	size_t bufsz;
	MdbSQL *sql;
	FILE *in = NULL, *out = NULL;
	char *filename_in=NULL, *filename_out=NULL;
#ifdef HAVE_READLINE_HISTORY
	char *home = getenv("HOME");
	char *histpath;
#endif
	char *delimiter = NULL;
	int in_from_colon_r = 0;
	char *locale = NULL;
	int print_mdbver = 0;

	GOptionEntry entries[] = {
		{ "delim", 'd', 0, G_OPTION_ARG_STRING, &delimiter, "Use this delimiter.", "char"},
		{ "no-pretty-print", 'P', G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &pretty_print, "Don't pretty print", NULL},
		{ "no-header", 'H', G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &headers, "Don't print header", NULL},
		{ "no-footer", 'F', G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &footers, "Don't print footer", NULL},
		{ "input", 'i', 0, G_OPTION_ARG_FILENAME, &filename_in, "Read SQL from specified file", "file"},
		{ "output", 'o', 0, G_OPTION_ARG_FILENAME, &filename_out, "Write result to specified file", "file"},
		{"version", 0, 0, G_OPTION_ARG_NONE, &print_mdbver, "Show mdbtools version and exit", NULL},
		{ NULL },
	};
	GError *error = NULL;
	GOptionContext *opt_context;

	opt_context = g_option_context_new("<file> - Run SQL");
	g_option_context_add_main_entries(opt_context, entries, NULL /*i18n*/);
	// g_option_context_set_strict_posix(opt_context, TRUE); /* options first, requires glib 2.44 */
	locale = setlocale(LC_CTYPE, "");
	if (!g_option_context_parse (opt_context, &argc, &argv, &error))
	{
		fprintf(stderr, "option parsing failed: %s\n", error->message);
		fputs(g_option_context_get_help(opt_context, TRUE, NULL), stderr);
		exit (1);
	}
	if (print_mdbver) {
		if (argc > 1) {
			fputs(g_option_context_get_help(opt_context, TRUE, NULL), stderr);
		}
		fprintf(stdout,"%s\n", MDB_FULL_VERSION);
		exit(argc > 1);
	}
	setlocale(LC_CTYPE, locale);

	if (argc > 2) {
		fputs("Wrong number of arguments.\n\n", stderr);
		fputs(g_option_context_get_help(opt_context, TRUE, NULL), stderr);
		exit(1);
	}

#ifdef HAVE_READLINE_HISTORY
	if (home) {
		histpath = (char *) g_strconcat(home, "/", HISTFILE, NULL);
		read_history(histpath);
		g_free(histpath);
	}
#endif
	/* If input is coming from a pipe */
	if (!isatty(fileno(stdin))) {
		in = stdin;
	}
	if (filename_in) {
		if (!strcmp(filename_in, "stdin"))
			in = stdin;
		else if (!(in = fopen(filename_in, "r"))) {
			fprintf(stderr, "Unable to open file %s\n", filename_in);
			exit(1);
		}
	}
	if (filename_out) {
		if (!(out = fopen(filename_out, "w"))) {
			fprintf(stderr,"Unable to open file %s\n", filename_out);
			exit(1);
		}
	}

	setlocale(LC_COLLATE, "");
	/* initialize the SQL engine */
	sql = mdb_sql_init();
	if (argc == 2) {
		mdb_sql_open(sql, argv[1]);
	}

	/* give the buffer an initial size */
	bufsz = 4096;
	mybuf = malloc(bufsz);
	mybuf[0]='\0';

	while (1) {
		line ++;
		if (s) {
			free(s);
			s = NULL;
		}

		if (in) {
			s=calloc(bufsz, 1);
			if (!fgets(s, (int)bufsz, in)) {
				// Backwards compatibility with older MDBTools
				// Files read from the command line had an
				// implicit "go" at the end
				if (!in_from_colon_r && strlen(mybuf))
					strcpy(s, "go");
				else if (in_from_colon_r) {
					line = 0;
					fclose(in);
					in = NULL;
					in_from_colon_r = 0;
				} else
					break;
			} else if (s[strlen(s)-1]=='\n')
				s[strlen(s)-1]=0;
		} else {
			snprintf(prompt, sizeof(prompt), "%d => ", line);
			locale = setlocale(LC_CTYPE, "");
			char *l = readline(prompt);
			setlocale(LC_CTYPE, locale);
			if (!l)
				break;
			s=g_locale_to_utf8(l, -1, NULL, NULL, NULL);
			free(l);
		}

		if (!strcmp(s,"exit") || !strcmp(s,"quit") || !strcmp(s,"bye"))
			break;

		if (line==1 && (!strncmp(s,"set ",4) || !strcmp(s,"set"))) {
			do_set_cmd(sql, &s[3]);
			line = 0;
		} else if (!strcmp(s,"go")) {
			line = 0;
			run_query((out) ? out : stdout, sql, mybuf, delimiter);
			mybuf[0]='\0';
		} else if (!strcmp(s,"reset")) {
			line = 0;
			mybuf[0]='\0';
		} else if (!strncmp(s,":r",2)) {
			char *fname = &s[2];
			if (in) {
				fprintf(stderr, "Can not handle nested opens\n");
			} else {
				while (*fname && isspace((int)*fname))
					fname++;
				if (!(in = fopen(fname, "r"))) {
					fprintf(stderr,"Unable to open file %s\n", fname);
					mybuf[0]=0;
				} else {
					in_from_colon_r = 1;
				}
			}
		} else {
			char *p;

			while (strlen(mybuf) + strlen(s) > bufsz) {
				bufsz *= 2;
				mybuf = realloc(mybuf, bufsz);
			}
#ifdef HAVE_READLINE_HISTORY
			/* don't record blank lines, or lines read from files
			 * specified on the command line */
			if ((!in || in_from_colon_r) && strlen(s))
				add_history(s);
#endif
			strcat(mybuf,s);
			/* preserve line numbering for the parser */
			strcat(mybuf,"\n");

			if ((p = find_sql_terminator(mybuf))) {
				*p = '\0';
				line = 0;
				run_query((out) ? out : stdout, sql, mybuf, delimiter);
				mybuf[0]='\0';
			}
		}
	}
	mdb_sql_exit(sql);

	free(mybuf);
	if (s) free(s);
	if (out) fclose(out);
	if ((in) && (in != stdin)) fclose(in);

#ifdef HAVE_READLINE_HISTORY
	if (home) {
		histpath = (char *) g_strconcat(home, "/", HISTFILE, NULL);
		write_history(histpath);
		g_free(histpath);
		clear_history();
	}
#endif

	g_option_context_free(opt_context);
	g_free(delimiter);
	g_free(filename_in);
	g_free(filename_out);

	return 0;
}
