/* 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 "mdbtools.h"
#include "mdbver.h"

#define EXPORT_BIND_SIZE 200000

#define is_binary_type(x) (x==MDB_OLE || x==MDB_BINARY || x==MDB_REPID)

static char *escapes(char *s);
static void format_value(FILE *outfile, char *value, size_t length, int quote_text, int col_type, char *escape_char, char *quote_char, int bin_mode, int export_flags, char *backend_name);

int
main(int argc, char **argv)
{
	unsigned int i;
	MdbHandle *mdb;
	MdbTableDef *table;
	MdbColumn *col;
	char **bound_values;
	int  *bound_lens;
	FILE *outfile = stdout;
	char *delimiter = NULL;
	char *row_delimiter = NULL;
	char *quote_char = NULL;
	char *escape_char = NULL;
	int header_row = 1;
	int quote_text = 1;
	int boolean_words = 0;
	int batch_size = 1000;
	int escape_cr_lf = 0;
	char *insert_dialect = NULL;
	char *shortdate_fmt = NULL;
	char *date_fmt = NULL;
	char *namespace = NULL;
	char *str_bin_mode = NULL;
	char *null_text = NULL;
	int export_flags = 0;
	int bin_mode = 0;
	char *value;
	size_t length;
	int ret;
	char *locale = NULL;
	char *table_name = NULL;
	int print_mdbver = 0;

	GOptionEntry entries[] = {
		{"no-header", 'H', G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &header_row, "Suppress header row.", NULL},
		{"delimiter", 'd', 0, G_OPTION_ARG_STRING, &delimiter, "Specify an alternative column delimiter. Default is comma.", "char"},
		{"row-delimiter", 'R', 0, G_OPTION_ARG_STRING, &row_delimiter, "Specify a row delimiter", "char"},
		{"no-quote", 'Q', G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &quote_text, "Don't wrap text-like fields in quotes.", NULL},
		{"quote", 'q', 0, G_OPTION_ARG_STRING, &quote_char, "Use <char> to wrap text-like fields. Default is double quote.", "char"},
		{"escape", 'X', 0, G_OPTION_ARG_STRING, &escape_char, "Use <char> to escape quoted characters within a field. Default is doubling.", "format"},
		{"escape-invisible", 'e', 0, G_OPTION_ARG_NONE, &escape_cr_lf, "Use C-style escaping for return (\\r), tab (\\t), line-feed (\\n), and back-slash (\\\\) characters. Default is to leave as they are.", NULL},
		{"insert", 'I', 0, G_OPTION_ARG_STRING, &insert_dialect, "INSERT statements (instead of CSV)", "backend"},
		{"namespace", 'N', 0, G_OPTION_ARG_STRING, &namespace, "Prefix identifiers with namespace", "namespace"},
		{"batch-size", 'S', 0, G_OPTION_ARG_INT, &batch_size, "Size of insert batches on supported platforms.", "int"},
		{"date-format", 'D', 0, G_OPTION_ARG_STRING, &shortdate_fmt, "Set the date format (see strftime(3) for details)", "format"},
		{"datetime-format", 'T', 0, G_OPTION_ARG_STRING, &date_fmt, "Set the date/time format (see strftime(3) for details)", "format"},
		{"null", '0', 0, G_OPTION_ARG_STRING, &null_text, "Use <char> to represent a NULL value", "char"},
		{"bin", 'b', 0, G_OPTION_ARG_STRING, &str_bin_mode, "Binary export mode", "strip|raw|octal|hex"},
		{"boolean-words", 'B', 0, G_OPTION_ARG_NONE, &boolean_words, "Use TRUE/FALSE in Boolean fields (default is 0/1)", NULL},
		{"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> <table> - export data from MDB file");
	g_option_context_add_main_entries(opt_context, entries, NULL /*i18n*/);
	locale = setlocale(LC_CTYPE, "");
	// g_option_context_set_strict_posix(opt_context, TRUE); /* options first, requires glib 2.44 */
	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);
	}
	if (argc != 3) {
		fputs("Wrong number of arguments.\n\n", stderr);
		fputs(g_option_context_get_help(opt_context, TRUE, NULL), stderr);
		exit(1);
	}
	table_name = g_locale_to_utf8(argv[2], -1, NULL, NULL, &error);
	if (!table_name) {
		fprintf(stderr, "argument parsing failed: %s\n", error->message);
		exit(1);
	}
	setlocale(LC_CTYPE, locale);

	/* Process options */
	if (quote_char)
		quote_char = escapes(quote_char);
    else if (insert_dialect && !strcmp(insert_dialect, "postgres"))
        quote_char = g_strdup("'");
    else
		quote_char = g_strdup("\"");

	if (delimiter)
		delimiter = escapes(delimiter);
	else
		delimiter = g_strdup(",");

	if (row_delimiter)
		row_delimiter = escapes(row_delimiter);
	else
		row_delimiter = g_strdup("\n");

	if (escape_char)
		escape_char = escapes(escape_char);

	if (insert_dialect)
		header_row = 0;

	if (null_text)
		null_text = escapes(null_text);
	else
		null_text = g_strdup("");

	if (str_bin_mode) {
		if (!strcmp(str_bin_mode, "strip"))
			bin_mode = MDB_BINEXPORT_STRIP;
		else if (!strcmp(str_bin_mode, "raw"))
			bin_mode = MDB_BINEXPORT_RAW;
		else if (!strcmp(str_bin_mode, "octal"))
			bin_mode = MDB_BINEXPORT_OCTAL;
		else if (!strcmp(str_bin_mode, "hex"))
			bin_mode = MDB_BINEXPORT_HEXADECIMAL;
		else {
			fputs("Invalid binary mode\n", stderr);
			exit(1);
		}
	} else {
		bin_mode = MDB_BINEXPORT_RAW;
    }

	if (escape_cr_lf) {
		export_flags |= MDB_EXPORT_ESCAPE_CONTROL_CHARS;
	}

	/* Open file */
	if (!(mdb = mdb_open(argv[1], MDB_NOFLAGS))) {
		/* Don't bother clean up memory before exit */
		exit(1);
	}

	if (date_fmt)
		mdb_set_date_fmt(mdb, date_fmt);

	if (shortdate_fmt)
		mdb_set_shortdate_fmt(mdb, shortdate_fmt);

	if (boolean_words)
		mdb_set_boolean_fmt_words(mdb);

    mdb_set_bind_size(mdb, EXPORT_BIND_SIZE);

	if (insert_dialect)
		if (!mdb_set_default_backend(mdb, insert_dialect)) {
			fputs("Invalid backend type\n", stderr);
			/* Don't bother clean up memory before exit */
			exit(1);
		}

	table = mdb_read_table_by_name(mdb, table_name, MDB_TABLE);
	if (!table) {
		fprintf(stderr, "Error: Table %s does not exist in this database.\n", table_name);
		/* Don't bother clean up memory before exit */
		exit(1);
	}

	/* read table */
	mdb_read_columns(table);
	mdb_rewind_table(table);

	bound_values = g_malloc(table->num_cols * sizeof(char *));
	bound_lens = g_malloc(table->num_cols * sizeof(int));
	for (i = 0; i < table->num_cols; i++) {
		/* bind columns */
		bound_values[i] = g_malloc0(EXPORT_BIND_SIZE);
		ret = mdb_bind_column(table, i + 1, bound_values[i], &bound_lens[i]);
		if (ret == -1) {
			fprintf(stderr, "Failed to bind column %d\n", i + 1);
			exit(1);
		}
	}
	if (header_row) {
		for (i = 0; i < table->num_cols; i++) {
			col = g_ptr_array_index(table->columns, i);
			if (i)
				fputs(delimiter, outfile);
			fputs(col->name, outfile);
		}
		fputs(row_delimiter, outfile);
	}

	// TODO refactor this into functions
	if (mdb->default_backend->capabilities & MDB_SHEXP_BULK_INSERT) {
		//for efficiency do multi row insert on engines that support this
		int counter = 0;
		while (mdb_fetch_row(table)) {
			if (counter % batch_size == 0) {
				counter = 0; // reset to 0, prevent overflow on extremely large data sets.
				char *quoted_name;
				quoted_name = mdb->default_backend->quote_schema_name(namespace, table_name);
				quoted_name = mdb_normalise_and_replace(mdb, &quoted_name);
				fprintf(outfile, "INSERT INTO %s (", quoted_name);
				free(quoted_name);
				for (i = 0; i < table->num_cols; i++) {
					if (i > 0) fputs(", ", outfile);
					col = g_ptr_array_index(table->columns, i);
					quoted_name = mdb->default_backend->quote_schema_name(NULL, col->name);
					quoted_name = mdb_normalise_and_replace(mdb, &quoted_name);
					fputs(quoted_name, outfile);
					free(quoted_name);
				}
				fputs(") VALUES ", outfile);
			} else {
				fputs(", ", outfile);
			}
			fputs("(", outfile);
			for (i = 0; i < table->num_cols; i++) {
				if (i > 0)
					fputs(delimiter, outfile);
				col = g_ptr_array_index(table->columns, i);
				if (!bound_lens[i]) {
					/* Don't quote NULLs */
					if (insert_dialect)
						fputs("NULL", outfile);
					else
						fputs(null_text, outfile);
				} else {
					if (col->col_type == MDB_OLE) {
						value = mdb_ole_read_full(mdb, col, &length);
					} else {
						value = bound_values[i];
						length = bound_lens[i];
					}
					format_value(outfile, value, length,
						     quote_text, col->col_type,
						     escape_char, quote_char,
						     bin_mode, export_flags,
						     mdb->backend_name);
					if (col->col_type == MDB_OLE)
						free(value);
				}
			}
			fputs(")", outfile);
			if (counter % batch_size == batch_size - 1) {
				fputs(";", outfile);
				fputs(row_delimiter, outfile);
			}
			counter++;
		}
		if (counter % batch_size != 0) {
			//if our last row did not land on closing tag, close the stement here
			fputs(";", outfile);
			fputs(row_delimiter, outfile);
		}
	} else {
		while (mdb_fetch_row(table)) {

			if (insert_dialect) {
				char *quoted_name;
				quoted_name = mdb->default_backend->quote_schema_name(namespace, table_name);
				quoted_name = mdb_normalise_and_replace(mdb, &quoted_name);
				fprintf(outfile, "INSERT INTO %s (", quoted_name);
				free(quoted_name);
				for (i = 0; i < table->num_cols; i++) {
					if (i > 0) fputs(", ", outfile);
					col = g_ptr_array_index(table->columns, i);
					quoted_name = mdb->default_backend->quote_schema_name(NULL, col->name);
					quoted_name = mdb_normalise_and_replace(mdb, &quoted_name);
					fputs(quoted_name, outfile);
					free(quoted_name);
				}
				fputs(") VALUES (", outfile);
			}

			for (i = 0; i < table->num_cols; i++) {
				if (i > 0)
					fputs(delimiter, outfile);
				col = g_ptr_array_index(table->columns, i);
				if (!bound_lens[i]) {
					/* Don't quote NULLs */
					if (insert_dialect)
						fputs("NULL", outfile);
					else
						fputs(null_text, outfile);
				} else {
					if (col->col_type == MDB_OLE) {
						value = mdb_ole_read_full(mdb, col, &length);
					} else {
						value = bound_values[i];
						length = bound_lens[i];
					}
					format_value(outfile, value, length,
						     quote_text, col->col_type,
						     escape_char, quote_char,
						     bin_mode, export_flags,
						     mdb->backend_name);
					if (col->col_type == MDB_OLE)
						free(value);
				}
			}
			if (insert_dialect) fputs(");", outfile);
			fputs(row_delimiter, outfile);
		}
	}

	/* free the memory used to bind */
	for (i=0;i<table->num_cols;i++) {
		g_free(bound_values[i]);
	}
	g_free(bound_values);
	g_free(bound_lens);
	mdb_free_tabledef(table);

	mdb_close(mdb);
	g_option_context_free(opt_context);

	// g_free ignores NULL
	g_free(quote_char);
	g_free(delimiter);
	g_free(row_delimiter);
	g_free(insert_dialect);
	g_free(date_fmt);
	g_free(escape_char);
	g_free(namespace);
	g_free(str_bin_mode);
	g_free(table_name);
	return 0;
}

static void format_value(FILE *outfile, char *value, size_t length, int quote_text, int col_type, char *escape_char, char *quote_char, int bin_mode, int export_flags, char *backend_name)
{
	/* Correctly handle insertion of binary blobs into sqlite3 using the notation of X'1234ABCD...') */
	if (!strcmp(backend_name, "sqlite")
			&& is_binary_type(col_type)
			&& bin_mode == MDB_BINEXPORT_HEXADECIMAL) {
		char *quote_char_binary_sqlite = (char *) g_strdup("'");
		fputs("X", outfile);
		mdb_print_col(outfile, value, quote_text, col_type, length, quote_char_binary_sqlite, escape_char, bin_mode | export_flags);
		g_free (quote_char_binary_sqlite);
		/* Correctly handle insertion of binary blobs into MySQL using the notation of 0x1234ABCD...) */
	} else if (!strcmp(backend_name, "mysql")
			&& is_binary_type(col_type)
			&& bin_mode == MDB_BINEXPORT_HEXADECIMAL) {
		char *quote_char_binary_sqlite = (char *) g_strdup("");
		fputs("0x", outfile);
		mdb_print_col(outfile, value, quote_text, col_type, length, quote_char_binary_sqlite, escape_char, bin_mode | export_flags);
		g_free (quote_char_binary_sqlite);
		/* Correctly handle insertion of binary blobs into PostgreSQL using the notation of decode('1234ABCD...', 'hex') */
	} else if (!strcmp(backend_name, "postgres")
			&& is_binary_type(col_type)
			&& bin_mode == MDB_BINEXPORT_HEXADECIMAL) {
		char *quote_char_binary_postgres = (char *) g_strdup("'");
		fputs("decode(", outfile);
		mdb_print_col(outfile, value, quote_text, col_type, length, quote_char_binary_postgres, escape_char, bin_mode | export_flags);
		fputs(", 'hex')", outfile);
		g_free (quote_char_binary_postgres);
		/* No special treatment for other backends or when hexadecimal notation hasn't been selected with the -b hex command line option */
	} else {
		mdb_print_col(outfile, value, quote_text, col_type, length, quote_char, escape_char, bin_mode | export_flags);
	}
}

static char *escapes(char *s)
{
	char *d = (char *) g_strdup(s);
	char *t = d;
	char *orig = s;
	unsigned char encode = 0;

	for (;*s; s++) {
		if (encode) {
			switch (*s) {
			case 'n': *t++='\n'; break;
			case 't': *t++='\t'; break;
			case 'r': *t++='\r'; break;
			default: *t++='\\'; *t++=*s; break;
			}
			encode=0;
		} else if (*s=='\\') {
			encode=1;
		} else {
			*t++=*s;
		}
	}
	*t='\0';
	g_free(orig);
	return d;
}
