/*
 *      dbm_gdm.c
 *
 *      Copyright 2010 Alexander Petukhov <devel(at)apetukhov.ru>
 *
 *      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.
 */

/*
 * 		Implementation of struct _dbg_module for GDB
 */

#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <wctype.h>
#include <unistd.h>

#ifdef HAVE_CONFIG_H
	#include "config.h"
#endif
#include <geanyplugin.h>
extern GeanyData		*geany_data;

#include "breakpoint.h"
#include "debug_module.h"
#include "gdb_mi.h"

/* module features */
#define MODULE_FEATURES MF_ASYNC_BREAKS

/* GDB spawn flags */
#define GDB_SPAWN_FLAGS \
	G_SPAWN_SEARCH_PATH | \
	G_SPAWN_DO_NOT_REAP_CHILD

/* GDB prompt */
#define GDB_PROMPT "(gdb) \n"

/* enumeration for GDB command execution status */
typedef enum _result_class {
	RC_DONE,
	RC_EXIT,
	RC_ERROR
} result_class;

/* structure to keep async command data (command line, messages) */
typedef struct _queue_item {
	gchar *message;
	gchar *command;
	gchar *error_message;
	gboolean format_error_message;
} queue_item;

/* enumeration for stop reason */
enum sr {
	SR_BREAKPOINT_HIT,
	SR_END_STEPPING_RANGE,
	SR_EXITED_NORMALLY,
	SR_SIGNAL_RECIEVED,
	SR_EXITED_SIGNALLED,
	SR_EXITED_WITH_CODE,
} stop_reason;

/* callbacks to use for messaging, error reporting and state change alerting */
static dbg_callbacks* dbg_cbs;

/* GDB command line arguments*/
static const gchar *gdb_args[] = { "gdb", "-i=mi", NULL };

/* GDB pid*/
static GPid gdb_pid = 0;

/* target pid*/
static GPid target_pid = 0;

/* GSource to watch GDB exit */
static guint gdb_src_id;

/* channels for GDB input/output */
static gint gdb_in;
static gint gdb_out;
static GIOChannel *gdb_ch_in;
static GIOChannel *gdb_ch_out;

/* GDB output event source id */
static guint gdb_id_out;

/* buffer for the error message */
char err_message[1000];

/* flag, showing that on debugger stop we have to call a callback */
gboolean requested_interrupt = FALSE;

/* autos list */
static GList *autos = NULL;

/* watches list */
static GList *watches = NULL;

/* loaded files list */
static GList *files = NULL;

/* set to true if library was loaded/unloaded
and it's nessesary to refresh files list */
static gboolean file_refresh_needed = FALSE;

/* current frame number */
static int active_frame = 0;

/* forward declarations */
static void stop(void);
static variable* add_watch(gchar* expression);
static void update_watches(void);
static void update_autos(void);
static void update_files(void);

/*
 * print message using color, based on message type
 */
static void colorize_message(gchar *message)
{
	const gchar *color;
	if ('=' == *message)
		color = "rose";
	else if ('^' == *message)
		color = "brown";
	else if ('*' == *message)
		color = "blue";
	else if ('~' == *message)
		color = "grey";
	else
		color = "red";

	dbg_cbs->send_message(message, color);
}

/*
 * shutdown GIOChannel
 */
static void shutdown_channel(GIOChannel ** ch)
{
	if (*ch)
	{
		gint fd = g_io_channel_unix_get_fd(*ch);
		g_io_channel_shutdown(*ch, TRUE, NULL);
		g_io_channel_unref(*ch);
		*ch = NULL;
		if (fd >= 0)
		{
			close(fd);
		}
	}
}

/*
 * called on GDB exit
 */
static void on_gdb_exit(GPid pid, gint status, gpointer data)
{
	gdb_pid = target_pid = 0;
	g_spawn_close_pid(pid);
	shutdown_channel(&gdb_ch_in);
	shutdown_channel(&gdb_ch_out);

	/* delete autos */
	g_list_foreach(autos, (GFunc)g_free, NULL);
	g_list_free(autos);
	autos = NULL;

	/* delete watches */
	g_list_foreach(watches, (GFunc)g_free, NULL);
	g_list_free(watches);
	watches = NULL;

	/* delete files */
	g_list_foreach(files, (GFunc)g_free, NULL);
	g_list_free(files);
	files = NULL;

	g_source_remove(gdb_src_id);
	gdb_src_id = 0;

	dbg_cbs->set_exited(0);
}

/*
 * reads gdb_out until "(gdb)" prompt met
 */
static GList* read_until_prompt(void)
{
	GList *lines = NULL;

	gchar *line = NULL;
	gsize terminator;
	while (G_IO_STATUS_NORMAL == g_io_channel_read_line(gdb_ch_out, &line, NULL, &terminator, NULL))
	{
		if (!strcmp(GDB_PROMPT, line))
			break;

		line[terminator] = '\0';
		lines = g_list_prepend (lines, line);
	}

	return g_list_reverse(lines);
}

/*
 * write a command to a gdb channel and flush with a newlinw character
 */
static void gdb_input_write_line(const gchar *line)
{
	GIOStatus st;
	GError *err = NULL;
	gsize count;
	const char *p;
	char command[1000];
	g_snprintf(command, sizeof command, "%s\n", line);

	for (p = command; *p; p += count)
	{
		st = g_io_channel_write_chars(gdb_ch_in, p, strlen(p), &count, &err);
		if (err || (st == G_IO_STATUS_ERROR) || (st == G_IO_STATUS_EOF))
		{
			if (err)
			{
#ifdef DEBUG_OUTPUT
				dbg_cbs->send_message(err->message, "red");
#endif
				g_clear_error(&err);
			}
			break;
		}
	}

	st = g_io_channel_flush(gdb_ch_in, &err);
	if (err || (st == G_IO_STATUS_ERROR) || (st == G_IO_STATUS_EOF))
	{
		if (err)
		{
			#ifdef DEBUG_OUTPUT
			dbg_cbs->send_message(err->message, "red");
			#endif
			g_clear_error(&err);
		}
	}
}

/*
 * free memory occupied by a queue item
 */
static void free_queue_item(queue_item *item)
{
	g_free(item->message);
	g_free(item->command);
	g_free(item->error_message);
	g_free(item);
}

/*
 * free a list of "queue_item" structures
 */
static void free_commands_queue(GList *queue)
{
	/* all commands completed */
	queue = g_list_first(queue);
	g_list_foreach(queue, (GFunc)free_queue_item, NULL);
	g_list_free(queue);
}

/*
 * add a new command ("queue_item" structure) to a list
 */
static GList* add_to_queue(GList* queue, const gchar *message, const gchar *command, const gchar *error_message, gboolean format_error_message)
{
	queue_item *item = (queue_item*)g_malloc(sizeof(queue_item));

	memset((void*)item, 0, sizeof(queue_item));

	item->message = g_strdup(message);
	item->command = g_strdup(command);
	item->error_message = g_strdup(error_message);
	item->format_error_message = format_error_message;

	return g_list_append(queue, (gpointer)item);
}

/*
 * asyncronous output reader
 * reads from startup async commands.
 * looks for a command completion (normal or abnormal), if noraml - executes next command
 */
static void exec_async_command(const gchar* command);
static gboolean on_read_async_output(GIOChannel * src, GIOCondition cond, gpointer data)
{
	gchar *line;
	gsize length;
	struct gdb_mi_record *record;

	if (G_IO_STATUS_NORMAL != g_io_channel_read_line(src, &line, NULL, &length, NULL))
		return TRUE;

	record = gdb_mi_record_parse(line);

	if (record && record->type == '^')
	{
		/* got some result */

		GList *lines;
		GList *commands = (GList*)data;

		if (gdb_id_out)
		{
			g_source_remove(gdb_id_out);
			gdb_id_out = 0;
		}

		lines = read_until_prompt();
		g_list_foreach(lines, (GFunc)g_free, NULL);
		g_list_free (lines);

		if (!strcmp(record->klass, "done"))
		{
			/* command completed succesfully - run next command if exists */
			if (commands->next)
			{
				/* if there are commads left */
				queue_item *item;

				commands = commands->next;
				item = (queue_item*)commands->data;

				/* send message to debugger messages window */
				if (item->message)
				{
					dbg_cbs->send_message(item->message, "grey");
				}

				gdb_input_write_line(item->command);

				gdb_id_out = g_io_add_watch(gdb_ch_out, G_IO_IN, on_read_async_output, commands);
			}
			else
			{
				/* all commands completed */
				free_commands_queue(commands);

				/* removing read callback */
				if (gdb_id_out)
				{
					g_source_remove(gdb_id_out);
					gdb_id_out = 0;
				}

				/* update source files list */
				update_files();

				/* -exec-run */
				exec_async_command("-exec-run");
			}
		}
		else
		{
			queue_item *item = (queue_item*)commands->data;
			if(item->error_message)
			{
				if (item->format_error_message)
				{
					const gchar* gdb_msg = gdb_mi_result_var(record->first, "msg", GDB_MI_VAL_STRING);
					gchar *msg = g_strdup_printf(item->error_message, gdb_msg);

					dbg_cbs->report_error(msg);
					g_free(msg);
				}
				else
				{
					dbg_cbs->report_error(item->error_message);
				}
			}

			/* free commands queue */
			free_commands_queue(commands);

			stop();
		}
	}

	gdb_mi_record_free(record);
	g_free(line);

	return TRUE;
}

/*
 * asyncronous gdb output reader
 * looks for a stopped event, then notifies "debug" module and removes async handler
 */
enum dbs debug_get_state(void);
static gboolean on_read_from_gdb(GIOChannel * src, GIOCondition cond, gpointer data)
{
	gchar *line;
	gsize length;
	const gchar *id;
	struct gdb_mi_record *record;

	if (G_IO_STATUS_NORMAL != g_io_channel_read_line(src, &line, NULL, &length, NULL))
		return TRUE;

	record = gdb_mi_record_parse(line);

	if (! record || record->type != GDB_MI_TYPE_PROMPT)
	{
		line[length] = '\0';
		if ((record && '~' == record->type) || '~' == line[0])
		{
			colorize_message(line);
		}
		else
		{
			gchar *compressed = g_strcompress(line);
			colorize_message(compressed);
			g_free(compressed);
		}
	}

	if (! record)
	{
		g_free(line);
		return TRUE;
	}

	if (!target_pid &&
		/* FIXME: =thread-group-created doesn't seem to exist, at least not in latest GDB docs */
		(gdb_mi_record_matches(record, '=', "thread-group-created", "id", &id, NULL) ||
		 gdb_mi_record_matches(record, '=', "thread-group-started", "pid", &id, NULL)))
	{
		target_pid = atoi(id);
	}
	else if (gdb_mi_record_matches(record, '=', "thread-created", "id", &id, NULL))
	{
		dbg_cbs->add_thread(atoi(id));
	}
	else if (gdb_mi_record_matches(record, '=', "thread-exited", "id", &id, NULL))
	{
		dbg_cbs->remove_thread(atoi(id));
	}
	else if (gdb_mi_record_matches(record, '=', "library-loaded", NULL) ||
			 gdb_mi_record_matches(record, '=', "library-unloaded", NULL))
	{
		file_refresh_needed = TRUE;
	}
	else if (gdb_mi_record_matches(record, '*', "running", NULL))
		dbg_cbs->set_run();
	else if (gdb_mi_record_matches(record, '*', "stopped", NULL))
	{
		const gchar *reason;

		/* removing read callback (will pulling all output left manually) */
		if (gdb_id_out)
		{
			g_source_remove(gdb_id_out);
			gdb_id_out = 0;
		}

		/* looking for a reason to stop */
		if ((reason = gdb_mi_result_var(record->first, "reason", GDB_MI_VAL_STRING)) != NULL)
		{
			if (!strcmp(reason, "breakpoint-hit"))
				stop_reason = SR_BREAKPOINT_HIT;
			else if (!strcmp(reason, "end-stepping-range"))
				stop_reason = SR_END_STEPPING_RANGE;
			else if (!strcmp(reason, "signal-received"))
				stop_reason = SR_SIGNAL_RECIEVED;
			else if (!strcmp(reason, "exited-normally"))
				stop_reason = SR_EXITED_NORMALLY;
			else if (!strcmp(reason, "exited-signalled"))
				stop_reason = SR_EXITED_SIGNALLED;
			else if (!strcmp(reason, "exited"))
				stop_reason = SR_EXITED_WITH_CODE;
			/* FIXME: handle "location-reached" */
		}
		else
		{
			/* somehow, sometimes there can be no stop reason */
			stop_reason = SR_EXITED_NORMALLY;
		}

		if (SR_BREAKPOINT_HIT == stop_reason || SR_END_STEPPING_RANGE == stop_reason || SR_SIGNAL_RECIEVED == stop_reason)
		{
			const gchar *thread_id = gdb_mi_result_var(record->first, "thread-id", GDB_MI_VAL_STRING);

			active_frame = 0;

			if (SR_BREAKPOINT_HIT == stop_reason || SR_END_STEPPING_RANGE == stop_reason)
			{
				/* update autos */
				update_autos();

				/* update watches */
				update_watches();

				/* update files */
				if (file_refresh_needed)
				{
					update_files();
					file_refresh_needed = FALSE;
				}
			}
			else
			{
				if (!requested_interrupt)
				{
					gchar *msg = g_strdup_printf(_("Program received signal %s (%s)"),
					                             (gchar *) gdb_mi_result_var(record->first, "signal-name", GDB_MI_VAL_STRING),
					                             (gchar *) gdb_mi_result_var(record->first, "signal-meaning", GDB_MI_VAL_STRING));

					dbg_cbs->report_error(msg);
					g_free(msg);
				}
				else
					requested_interrupt = FALSE;
			}

			dbg_cbs->set_stopped(thread_id ? atoi(thread_id) : 0);
		}
		else if (stop_reason == SR_EXITED_NORMALLY || stop_reason == SR_EXITED_SIGNALLED || stop_reason == SR_EXITED_WITH_CODE)
		{
			if (stop_reason == SR_EXITED_WITH_CODE)
			{
				const gchar *exit_code = gdb_mi_result_var(record->first, "exit-code", GDB_MI_VAL_STRING);
				long int code = exit_code ? strtol(exit_code, NULL, 8) : 0;
				gchar *message;

				message = g_strdup_printf(_("Program exited with code \"%i\""), (int)(char)code);
				dbg_cbs->report_error(message);

				g_free(message);
			}

			stop();
		}
	}
	else if (gdb_mi_record_matches(record, '^', "error", NULL))
	{
		GList *lines, *iter;
		const gchar *msg = gdb_mi_result_var(record->first, "msg", GDB_MI_VAL_STRING);

		/* removing read callback (will pulling all output left manually) */
		if (gdb_id_out)
		{
			g_source_remove(gdb_id_out);
			gdb_id_out = 0;
		}

		/* set debugger stopped if is running */
		if (DBS_STOPPED != debug_get_state())
		{
			/* FIXME: does GDB/MI ever return a thread-id with an error?? */
			const gchar *thread_id = gdb_mi_result_var(record->first, "thread-id", GDB_MI_VAL_STRING);

			dbg_cbs->set_stopped(thread_id ? atoi(thread_id) : 0);
		}

		/* reading until prompt */
		lines = read_until_prompt();
		for (iter = lines; iter; iter = iter->next)
		{
			gchar *l = (gchar*)iter->data;
			if (strcmp(l, GDB_PROMPT))
				colorize_message(l);
			g_free(l);
		}
		g_list_free (lines);

		/* send error message */
		dbg_cbs->report_error(msg);
	}

	g_free(line);
	gdb_mi_record_free(record);

	return TRUE;
}

/*
 * execute "command" asyncronously
 * after writing command to an input channel
 * connects reader to output channel and exits
 * after execution
 */
static void exec_async_command(const gchar* command)
{
#ifdef DEBUG_OUTPUT
	dbg_cbs->send_message(command, "red");
#endif

	gdb_input_write_line(command);

	/* connect read callback to the output chanel */
	gdb_id_out = g_io_add_watch(gdb_ch_out, G_IO_IN, on_read_from_gdb, NULL);
}

/*
 * execute "command" syncronously
 * i.e. reading output right
 * after execution
 */
static result_class exec_sync_command(const gchar* command, gboolean wait4prompt, struct gdb_mi_record ** command_record)
{
	GList *lines, *iter;
	result_class rc;

#ifdef DEBUG_OUTPUT
	dbg_cbs->send_message(command, "red");
#endif

	/* write command to gdb input channel */
	gdb_input_write_line(command);

	if (!wait4prompt)
		return RC_DONE;

	if (command_record)
		*command_record = NULL;

	lines = read_until_prompt();

#ifdef DEBUG_OUTPUT
	for (iter = lines; iter; iter = iter->next)
	{
		dbg_cbs->send_message((gchar*)iter->data, "red");
	}
#endif

	rc = RC_ERROR;

	for (iter = lines; iter; iter = iter->next)
	{
		gchar *line = (gchar*)iter->data;
		struct gdb_mi_record *record = gdb_mi_record_parse(line);

		if (record && '^' == record->type)
		{
			if (gdb_mi_record_matches(record, '^', "done", NULL))
				rc = RC_DONE;
			else if (gdb_mi_record_matches(record, '^', "error", NULL))
			{
				/* save error message */
				const gchar *msg = gdb_mi_result_var(record->first, "msg", GDB_MI_VAL_STRING);
				strncpy(err_message, msg ? msg : "", G_N_ELEMENTS(err_message) - 1);

				rc = RC_ERROR;
			}
			else if (gdb_mi_record_matches(record, '^', "exit", NULL))
				rc = RC_EXIT;

			if (command_record)
			{
				*command_record = record;
				record = NULL;
			}
		}
		else if (! record || '&' != record->type)
		{
			colorize_message (line);
		}
		gdb_mi_record_free(record);
	}

	g_list_foreach(lines, (GFunc)g_free, NULL);
	g_list_free(lines);

	return rc;
}


/* escapes @str so it is valid to put it inside a quoted argument
 * escapes '\' and '"'
 * unlike g_strescape(), it doesn't escape non-ASCII characters so keeps
 * all of UTF-8 */
static gchar *escape_string(const gchar *str)
{
	gchar *new = g_malloc(strlen(str) * 2 + 1);
	gchar *p;

	for (p = new; *str; str++) {
		switch (*str) {
			/* FIXME: what to do with '\n'?  can't seem to find a way to escape it */
			case '\\':
			case '"':
				*p++ = '\\';
				/* fallthrough */
			default:
				*p++ = *str;
		}
	}
	*p = 0;

	return new;
}

/*
 * starts gdb, collects commands and start the first one
 */
static gboolean run(const gchar* file, const gchar* commandline, GList* env, GList *witer, GList *biter, const gchar* terminal_device, dbg_callbacks* callbacks)
{
	const gchar *exclude[] = { "LANG", NULL };
	gchar **gdb_env = utils_copy_environment(exclude, "LANG", "C", NULL);
	gchar *working_directory = g_path_get_dirname(file);
	GList *lines, *iter;
	GList *commands = NULL;
	gchar *command;
	gchar *escaped;
	int bp_index;
	queue_item *item;

	dbg_cbs = callbacks;

	/* spawn GDB */
	if (!g_spawn_async_with_pipes(working_directory, (gchar**)gdb_args, gdb_env,
				     GDB_SPAWN_FLAGS, NULL,
				     NULL, &gdb_pid, &gdb_in, &gdb_out, NULL, NULL))
	{
		dbg_cbs->report_error(_("Failed to spawn gdb process"));
		g_free(working_directory);
		g_strfreev(gdb_env);
		return FALSE;
	}
	g_free(working_directory);
	g_strfreev(gdb_env);

	/* move gdb to it's own process group */
	setpgid(gdb_pid, 0);

	/* set handler for gdb process exit event */
	gdb_src_id = g_child_watch_add(gdb_pid, on_gdb_exit, NULL);

	/* create GDB GIO chanels */
	gdb_ch_in = g_io_channel_unix_new(gdb_in);
	gdb_ch_out = g_io_channel_unix_new(gdb_out);

	/* reading starting gdb messages */
	lines = read_until_prompt();
	for (iter = lines; iter; iter = iter->next)
	{
		gchar *unescaped = g_strcompress((gchar*)iter->data);
		if (strlen(unescaped))
		{
			colorize_message((gchar*)iter->data);
		}
	}
	g_list_foreach(lines, (GFunc)g_free, NULL);
	g_list_free(lines);

	/* add initial watches to the list */
	while (witer)
	{
		gchar *name = (gchar*)witer->data;

		variable *var = variable_new(name, VT_WATCH);
		watches = g_list_append(watches, var);

		witer = witer->next;
	}

	/* collect commands */

	/* loading file */
	escaped = escape_string(file);
	command = g_strdup_printf("-file-exec-and-symbols \"%s\"", escaped);
	commands = add_to_queue(commands, _("~\"Loading target file.\\n\""), command, _("Error loading file"), FALSE);
	g_free(command);
	g_free(escaped);

	/* setting asyncronous mode */
	commands = add_to_queue(commands, NULL, "-gdb-set target-async 1", _("Error configuring GDB"), FALSE);

	/* setting null-stop array printing */
	commands = add_to_queue(commands, NULL, "-interpreter-exec console \"set print null-stop\"", _("Error configuring GDB"), FALSE);

	/* enable pretty printing */
	commands = add_to_queue(commands, NULL, "-enable-pretty-printing", _("Error configuring GDB"), FALSE);

	/* set locale */
	command = g_strdup_printf("-gdb-set environment LANG=%s", g_getenv("LANG"));
	commands = add_to_queue(commands, NULL, command, NULL, FALSE);
	g_free(command);

	/* set arguments */
	command = g_strdup_printf("-exec-arguments %s", commandline);
	commands = add_to_queue(commands, NULL, command, NULL, FALSE);
	g_free(command);

	/* set passed evironment */
	iter = env;
	while (iter)
	{
		gchar *name, *value;

		name = (gchar*)iter->data;
		iter = iter->next;
		value = (gchar*)iter->data;

		command = g_strdup_printf("-gdb-set environment %s=%s", name, value);
		commands = add_to_queue(commands, NULL, command, NULL, FALSE);
		g_free(command);

		iter = iter->next;
	}

	/* set breaks */
	bp_index = 1;
	while (biter)
	{
		breakpoint *bp = (breakpoint*)biter->data;
		gchar *error_message;

		escaped = escape_string(bp->file);
		command = g_strdup_printf("-break-insert -f \"\\\"%s\\\":%i\"", escaped, bp->line);
		g_free(escaped);

		error_message = g_strdup_printf(_("Breakpoint at %s:%i cannot be set\nDebugger message: %s"), bp->file, bp->line, "%s");

		commands = add_to_queue(commands, NULL, command, error_message, TRUE);

		g_free(command);

		if (bp->hitscount)
		{
			command = g_strdup_printf("-break-after %i %i", bp_index, bp->hitscount);
			commands = add_to_queue(commands, NULL, command, error_message, TRUE);
			g_free(command);
		}
		if (strlen(bp->condition))
		{
			command = g_strdup_printf ("-break-condition %i %s", bp_index, bp->condition);
			commands = add_to_queue(commands, NULL, command, error_message, TRUE);
			g_free(command);
		}
		if (!bp->enabled)
		{
			command = g_strdup_printf ("-break-disable %i", bp_index);
			commands = add_to_queue(commands, NULL, command, error_message, TRUE);
			g_free(command);
		}

		g_free(error_message);

		bp_index++;
		biter = biter->next;
	}

	/* set debugging terminal */
	command = g_strconcat("-inferior-tty-set ", terminal_device, NULL);
	commands = add_to_queue(commands, NULL, command, NULL, FALSE);
	g_free(command);

	/* connect read callback to the output chanel */
	gdb_id_out = g_io_add_watch(gdb_ch_out, G_IO_IN, on_read_async_output, commands);

	item = (queue_item*)commands->data;

	/* send message to debugger messages window */
	if (item->message)
	{
		dbg_cbs->send_message(item->message, "grey");
	}

	/* send first command */
	gdb_input_write_line(item->command);

	return TRUE;
}

/*
 * starts debugging
 */
static void restart(void)
{
	dbg_cbs->clear_messages();
	exec_async_command("-exec-run");
}

/*
 * stops GDB
 */
static void stop(void)
{
	exec_sync_command("-gdb-exit", FALSE, NULL);
}

/*
 * resumes GDB
 */
static void resume(void)
{
	exec_async_command("-exec-continue");
}

/*
 * step over
 */
static void step_over(void)
{
	exec_async_command("-exec-next");
}

/*
 * step into
 */
static void step_into(void)
{
	exec_async_command("-exec-step");
}

/*
 * step out
 */
static void step_out(void)
{
	exec_async_command("-exec-finish");
}

/*
 * execute until
 */
static void execute_until(const gchar *file, int line)
{
	gchar command[1000];
	g_snprintf(command, sizeof command, "-exec-until %s:%i", file, line);
	exec_async_command(command);
}

/*
 * gets breakpoint number by file and line
 */
static int get_break_number(char* file, int line)
{
	struct gdb_mi_record *record;
	const struct gdb_mi_result *table, *body, *bkpt;
	int break_number = -1;

	exec_sync_command("-break-list", TRUE, &record);
	if (! record)
		return -1;

	table = gdb_mi_result_var(record->first, "BreakpointTable", GDB_MI_VAL_LIST);
	body = gdb_mi_result_var(table, "body", GDB_MI_VAL_LIST);
	gdb_mi_result_foreach_matched (bkpt, body, "bkpt", GDB_MI_VAL_LIST)
	{
		const gchar *number = gdb_mi_result_var(bkpt->val->v.list, "number", GDB_MI_VAL_STRING);
		const gchar *location = gdb_mi_result_var(bkpt->val->v.list, "original-location", GDB_MI_VAL_STRING);
		const gchar *colon;
		gboolean break_found = FALSE;

		if (! number || ! location)
			continue;

		colon = strrchr(location, ':');
		if (colon && atoi(colon + 1) == line)
		{
			gchar *fname;

			/* quotes around filename (location not found or something?) */
			if (*location == '"' && colon - location > 2)
				fname = g_strndup(location + 1, colon - location - 2);
			else
				fname = g_strndup(location, colon - location);
			break_found = strcmp(fname, file) == 0;
			g_free(fname);
		}
		if (break_found)
		{
			break_number = atoi(number);
			break;
		}
	}

	gdb_mi_record_free(record);

	return break_number;
}

/*
 * set breakpoint
 */
static gboolean set_break(breakpoint* bp, break_set_activity bsa)
{
	char command[1000];
	if (BSA_NEW_BREAK == bsa)
	{
		/* new breakpoint */
		struct gdb_mi_record *record;
		const struct gdb_mi_result *bkpt;
		const gchar *number;
		gchar *escaped;
		int num = 0;

		/* 1. insert breakpoint */
		escaped = escape_string(bp->file);
		g_snprintf(command, sizeof command, "-break-insert \"\\\"%s\\\":%i\"", escaped, bp->line);
		if (RC_DONE != exec_sync_command(command, TRUE, &record) || !record)
		{
			gdb_mi_record_free(record);
			record = NULL;
			g_snprintf(command, sizeof command, "-break-insert -f \"\\\"%s\\\":%i\"", escaped, bp->line);
			if (RC_DONE != exec_sync_command(command, TRUE, &record) || !record)
			{
				gdb_mi_record_free(record);
				g_free(escaped);
				return FALSE;
			}
		}
		/* lookup break-number */
		bkpt = gdb_mi_result_var(record->first, "bkpt", GDB_MI_VAL_LIST);
		if ((number = gdb_mi_result_var(bkpt, "number", GDB_MI_VAL_STRING)))
			num = atoi(number);
		gdb_mi_record_free(record);
		g_free(escaped);
		/* 2. set hits count if differs from 0 */
		if (bp->hitscount)
		{
			g_snprintf(command, sizeof command, "-break-after %i %i", num, bp->hitscount);
			exec_sync_command(command, TRUE, NULL);
		}
		/* 3. set condition if exists */
		if (strlen(bp->condition))
		{
			g_snprintf(command, sizeof command, "-break-condition %i %s", num, bp->condition);
			if (RC_DONE != exec_sync_command(command, TRUE, NULL))
				return FALSE;
		}
		/* 4. disable if disabled */
		if (!bp->enabled)
		{
			g_snprintf(command, sizeof command, "-break-disable %i", num);
			exec_sync_command(command, TRUE, NULL);
		}

		return TRUE;
	}
	else
	{
		/* modify existing breakpoint */
		int bnumber = get_break_number(bp->file, bp->line);
		if (-1 == bnumber)
			return FALSE;

		if (BSA_UPDATE_ENABLE == bsa)
			g_snprintf(command, sizeof command, bp->enabled ? "-break-enable %i" : "-break-disable %i", bnumber);
		else if (BSA_UPDATE_HITS_COUNT == bsa)
			g_snprintf(command, sizeof command, "-break-after %i %i", bnumber, bp->hitscount);
		else if (BSA_UPDATE_CONDITION == bsa)
			g_snprintf(command, sizeof command, "-break-condition %i %s", bnumber, bp->condition);

		return RC_DONE == exec_sync_command(command, TRUE, NULL);
	}

	return FALSE;
}

/*
 * removes breakpoint
 */
static gboolean remove_break(breakpoint* bp)
{
	/* find break number */
	int number = get_break_number(bp->file, bp->line);
	if (-1 != number)
	{
		result_class rc;
		gchar command[100];

		g_snprintf(command, sizeof command, "-break-delete %i", number);
		rc = exec_sync_command(command, TRUE, NULL);

		return RC_DONE == rc;
	}
	return FALSE;
}

/*
 * get active  frame
 */
static int get_active_frame(void)
{
	return active_frame;
}

/*
 * select frame
 */
static void set_active_frame(int frame_number)
{
	gchar *command = g_strdup_printf("-stack-select-frame %i", frame_number);
	if (RC_DONE == exec_sync_command(command, TRUE, NULL))
	{
		active_frame = frame_number;
		update_autos();
		update_watches();
	}
	g_free(command);
}

static int get_active_thread(void)
{
	struct gdb_mi_record *record = NULL;
	int current_thread = 0;

	if (RC_DONE == exec_sync_command("-thread-info", TRUE, &record))
	{
		const gchar *id = gdb_mi_result_var(record->first, "current-thread-id", GDB_MI_VAL_STRING);
		current_thread = id ? atoi(id) : 0;
	}
	gdb_mi_record_free(record);

	return current_thread;
}

static gboolean set_active_thread(int thread_id)
{
	gchar *command = g_strdup_printf("-thread-select %i", thread_id);
	gboolean success = (RC_DONE == exec_sync_command(command, TRUE, NULL));

	if (success)
		set_active_frame(0);

	g_free(command);

	return success;
}

/*
 * gets stack
 */
static GList* get_stack(void)
{
	struct gdb_mi_record *record = NULL;
	const struct gdb_mi_result *stack_node, *frame_node;
	GList *stack = NULL;

	if (RC_DONE != exec_sync_command("-stack-list-frames", TRUE, &record) || ! record)
	{
		gdb_mi_record_free(record);
		return NULL;
	}

	stack_node = gdb_mi_result_var(record->first, "stack", GDB_MI_VAL_LIST);
	gdb_mi_result_foreach_matched (frame_node, stack_node, "frame", GDB_MI_VAL_LIST)
	{
		const gchar *addr = gdb_mi_result_var(frame_node->val->v.list, "addr", GDB_MI_VAL_STRING);
		const gchar *func = gdb_mi_result_var(frame_node->val->v.list, "func", GDB_MI_VAL_STRING);
		const gchar *line = gdb_mi_result_var(frame_node->val->v.list, "line", GDB_MI_VAL_STRING);
		const gchar *file, *fullname;
		frame *f = frame_new();

		f->address = g_strdup(addr);
		f->function = g_strdup(func);

		/* file: fullname | file | from */
		if ((fullname = file = gdb_mi_result_var(frame_node->val->v.list, "fullname", GDB_MI_VAL_STRING)) ||
			(file = gdb_mi_result_var(frame_node->val->v.list, "file", GDB_MI_VAL_STRING)) ||
			(file = gdb_mi_result_var(frame_node->val->v.list, "from", GDB_MI_VAL_STRING)))
		{
			f->file = g_strdup(file);
		}
		else
		{
			f->file = g_strdup("");
		}

		/* whether source is available */
		f->have_source = fullname ? TRUE : FALSE;

		/* line */
		f->line = line ? atoi(line) : 0;

		stack = g_list_prepend(stack, f);
	}
	gdb_mi_record_free(record);

	return g_list_reverse(stack);
}

/*
 * updates variables from vars list
 */
static void get_variables (GList *vars)
{
	while (vars)
	{
		gchar command[1000];

		variable *var = (variable*)vars->data;

		gchar *varname = var->internal->str;
		struct gdb_mi_record *record = NULL;
		const gchar *expression = NULL;
		const gchar *numchild = NULL;
		const gchar *value = NULL;
		const gchar *type = NULL;

		/* path expression */
		g_snprintf(command, sizeof command, "-var-info-path-expression \"%s\"", varname);
		exec_sync_command(command, TRUE, &record);
		if (record)
			expression = gdb_mi_result_var(record->first, "path_expr", GDB_MI_VAL_STRING);
		g_string_assign(var->expression, expression ? expression : "");
		gdb_mi_record_free(record);

		/* children number */
		g_snprintf(command, sizeof command, "-var-info-num-children \"%s\"", varname);
		exec_sync_command(command, TRUE, &record);
		if (record)
			numchild = gdb_mi_result_var(record->first, "numchild", GDB_MI_VAL_STRING);
		var->has_children = numchild && atoi(numchild) > 0;
		gdb_mi_record_free(record);

		/* value */
		g_snprintf(command, sizeof command, "-data-evaluate-expression \"%s\"", var->expression->str);
		exec_sync_command(command, TRUE, &record);
		if (record)
			value = gdb_mi_result_var(record->first, "value", GDB_MI_VAL_STRING);
		if (!value)
		{
			gdb_mi_record_free(record);
			g_snprintf(command, sizeof command, "-var-evaluate-expression \"%s\"", varname);
			exec_sync_command(command, TRUE, &record);
			if (record)
				value = gdb_mi_result_var(record->first, "value", GDB_MI_VAL_STRING);
		}
		g_string_assign(var->value, value ? value : "");
		gdb_mi_record_free(record);

		/* type */
		g_snprintf(command, sizeof command, "-var-info-type \"%s\"", varname);
		exec_sync_command(command, TRUE, &record);
		if (record)
			type = gdb_mi_result_var(record->first, "type", GDB_MI_VAL_STRING);
		g_string_assign(var->type, type ? type : "");
		gdb_mi_record_free(record);

		vars = vars->next;
	}
}

/*
 * updates files list
 */
static void update_files(void)
{
	GHashTable *ht;
	struct gdb_mi_record *record = NULL;
	const struct gdb_mi_result *files_node;

	if (files)
	{
		/* free previous list */
		g_list_foreach(files, (GFunc)g_free, NULL);
		g_list_free(files);
		files = NULL;
	}

	exec_sync_command("-file-list-exec-source-files", TRUE, &record);
	if (! record)
		return;

	ht = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, NULL);

	files_node = gdb_mi_result_var(record->first, "files", GDB_MI_VAL_LIST);
	gdb_mi_result_foreach_matched (files_node, files_node, NULL, GDB_MI_VAL_LIST)
	{
		const gchar *fullname = gdb_mi_result_var(files_node->val->v.list, "fullname", GDB_MI_VAL_STRING);

		if (fullname && !g_hash_table_lookup(ht, fullname))
		{
			g_hash_table_insert(ht, (gpointer)fullname, (gpointer)1);
			files = g_list_append(files, g_strdup(fullname));
		}
	}

	g_hash_table_destroy(ht);
	gdb_mi_record_free(record);
}

/*
 * updates watches list
 */
static void update_watches(void)
{
	gchar command[1000];
	GList *updating = NULL;
	GList *iter;

	/* delete all GDB variables */
	for (iter = watches; iter; iter = iter->next)
	{
		variable *var = (variable*)iter->data;

		if (var->internal->len)
		{
			g_snprintf(command, sizeof command, "-var-delete %s", var->internal->str);
			exec_sync_command(command, TRUE, NULL);
		}

		/* reset all variables fields */
		variable_reset(var);
	}

	/* create GDB variables, adding successfully created
	variables to the list then passed for updatind */
	for (iter = watches; iter; iter = iter->next)
	{
		variable *var = (variable*)iter->data;
		struct gdb_mi_record *record = NULL;
		const gchar *name;
		gchar *escaped;

		/* try to create variable */
		escaped = escape_string(var->name->str);
		g_snprintf(command, sizeof command, "-var-create - * \"%s\"", escaped);
		g_free(escaped);

		if (RC_DONE != exec_sync_command(command, TRUE, &record) || !record)
		{
			/* do not include to updating list, move to next watch */
			var->evaluated = FALSE;
			g_string_assign(var->internal, "");
			gdb_mi_record_free(record);

			continue;
		}

		/* find and assign internal name */
		name = gdb_mi_result_var(record->first, "name", GDB_MI_VAL_STRING);
		g_string_assign(var->internal, name ? name : "");
		gdb_mi_record_free(record);

		var->evaluated = name != NULL;

		/* add to updating list */
		updating = g_list_prepend(updating, var);
	}
	updating = g_list_reverse(updating);

	/* update watches */
	get_variables(updating);

	/* free updating list */
	g_list_free(updating);
}

/*
 * updates autos list
 */
static void update_autos(void)
{
	gchar command[1000];
	GList *unevaluated = NULL, *vars = NULL, *iter;

	/* remove all previous GDB variables for autos */
	for (iter = autos; iter; iter = iter->next)
	{
		variable *var = (variable*)iter->data;

		g_snprintf(command, sizeof command, "-var-delete %s", var->internal->str);
		exec_sync_command(command, TRUE, NULL);
	}

	g_list_foreach(autos, (GFunc)variable_free, NULL);
	g_list_free(autos);
	autos = NULL;

	/* add current autos to the list */

	struct gdb_mi_record *record = NULL;

	g_snprintf(command, sizeof command, "-stack-list-arguments 0 %i %i", active_frame, active_frame);
	if (RC_DONE == exec_sync_command(command, TRUE, &record) && record)
	{
		const struct gdb_mi_result *stack_args = gdb_mi_result_var(record->first, "stack-args", GDB_MI_VAL_LIST);

		gdb_mi_result_foreach_matched (stack_args, stack_args, "frame", GDB_MI_VAL_LIST)
		{
			const struct gdb_mi_result *args = gdb_mi_result_var(stack_args->val->v.list, "args", GDB_MI_VAL_LIST);

			gdb_mi_result_foreach_matched (args, args, "name", GDB_MI_VAL_STRING)
			{
				variable *var = variable_new(args->val->v.string, VT_ARGUMENT);
				vars = g_list_append(vars, var);
			}
		}
	}
	gdb_mi_record_free(record);

	if (RC_DONE == exec_sync_command("-stack-list-locals 0", TRUE, &record) && record)
	{
		const struct gdb_mi_result *locals = gdb_mi_result_var(record->first, "locals", GDB_MI_VAL_LIST);

		gdb_mi_result_foreach_matched (locals, locals, "name", GDB_MI_VAL_STRING)
		{
			variable *var = variable_new(locals->val->v.string, VT_LOCAL);
			vars = g_list_append(vars, var);
		}
	}
	gdb_mi_record_free(record);

	for (iter = vars; iter; iter = iter->next)
	{
		variable *var = iter->data;
		struct gdb_mi_record *create_record = NULL;
		gchar *escaped;
		const gchar *intname;

		/* create new gdb variable */
		escaped = escape_string(var->name->str);
		g_snprintf(command, sizeof command, "-var-create - * \"%s\"", escaped);
		g_free(escaped);

		/* form new variable */
		if (RC_DONE == exec_sync_command(command, TRUE, &create_record) && create_record &&
			(intname = gdb_mi_result_var(create_record->first, "name", GDB_MI_VAL_STRING)))
		{
			var->evaluated = TRUE;
			g_string_assign(var->internal, intname);
			autos = g_list_append(autos, var);
		}
		else
		{
			var->evaluated = FALSE;
			g_string_assign(var->internal, "");
			unevaluated = g_list_append(unevaluated, var);
		}
		gdb_mi_record_free(create_record);
	}
	g_list_free(vars);

	/* get values for the autos (without incorrect variables) */
	get_variables(autos);

	/* add incorrect variables */
	autos = g_list_concat(autos, unevaluated);
}

/*
 * get autos list
 */
static GList* get_autos (void)
{
	return g_list_copy(autos);
}

/*
 * get watches list
 */
static GList* get_watches (void)
{
	return g_list_copy(watches);
}

/*
 * get files list
 */
static GList* get_files (void)
{
	return g_list_copy(files);
}

/*
 * get list of children
 */
static GList* get_children (gchar* path)
{
	GList *children = NULL;

	gchar command[1000];
	result_class rc;
	struct gdb_mi_record *record = NULL;
	const gchar *numchild;
	int n;

	/* children number */
	g_snprintf(command, sizeof command, "-var-info-num-children \"%s\"", path);
	rc = exec_sync_command(command, TRUE, &record);
	if (RC_DONE != rc || ! record)
	{
		gdb_mi_record_free(record);
		return NULL;
	}
	numchild = gdb_mi_result_var(record->first, "numchild", GDB_MI_VAL_STRING);
	n = numchild ? atoi(numchild) : 0;
	gdb_mi_record_free(record);
	if (!n)
		return NULL;

	/* recursive get children and put into list */
	g_snprintf(command, sizeof command, "-var-list-children \"%s\"", path);
	rc = exec_sync_command(command, TRUE, &record);
	if (RC_DONE == rc && record)
	{
		const struct gdb_mi_result *child_node = gdb_mi_result_var(record->first, "children", GDB_MI_VAL_LIST);

		gdb_mi_result_foreach_matched (child_node, child_node, "child", GDB_MI_VAL_LIST)
		{
			const gchar *internal = gdb_mi_result_var(child_node->val->v.list, "name", GDB_MI_VAL_STRING);
			const gchar *name = gdb_mi_result_var(child_node->val->v.list, "exp", GDB_MI_VAL_STRING);
			variable *var;

			if (! name || ! internal)
				continue;

			var = variable_new2(name, internal, VT_CHILD);
			var->evaluated = TRUE;

			children = g_list_prepend(children, var);
		}
	}
	gdb_mi_record_free(record);

	children = g_list_reverse(children);
	get_variables(children);

	return children;
}

/*
 * add new watch
 */
static variable* add_watch(gchar* expression)
{
	gchar command[1000];
	gchar *escaped;
	struct gdb_mi_record *record = NULL;
	const gchar *name;
	GList *vars = NULL;
	variable *var = variable_new(expression, VT_WATCH);

	watches = g_list_append(watches, var);

	/* try to create a variable */
	escaped = escape_string(var->name->str);
	g_snprintf(command, sizeof command, "-var-create - * \"%s\"", escaped);
	g_free(escaped);

	if (RC_DONE != exec_sync_command(command, TRUE, &record) || !record)
	{
		gdb_mi_record_free(record);
		return var;
	}

	name = gdb_mi_result_var(record->first, "name", GDB_MI_VAL_STRING);
	g_string_assign(var->internal, name ? name : "");
	var->evaluated = name != NULL;

	vars = g_list_append(NULL, var);
	get_variables(vars);

	gdb_mi_record_free(record);
	g_list_free(vars);

	return var;
}

/*
 * remove watch
 */
static void remove_watch(gchar* internal)
{
	GList *iter = watches;
	while (iter)
	{
		variable *var = (variable*)iter->data;
		if (!strcmp(var->internal->str, internal))
		{
			gchar command[1000];
			g_snprintf(command, sizeof command, "-var-delete %s", internal);
			exec_sync_command(command, TRUE, NULL);
			variable_free(var);
			watches = g_list_delete_link(watches, iter);
		}
		iter = iter->next;
	}
}

/*
 * evaluates given expression and returns the result
 */
static gchar *evaluate_expression(gchar *expression)
{
	struct gdb_mi_record *record = NULL;
	gchar *value;
	char command[1000];

	g_snprintf(command, sizeof command, "-data-evaluate-expression \"%s\"", expression);
	if (RC_DONE != exec_sync_command(command, TRUE, &record) || ! record)
	{
		gdb_mi_record_free(record);
		return NULL;
	}

	value = g_strdup(gdb_mi_result_var(record->first, "value", GDB_MI_VAL_STRING));
	gdb_mi_record_free(record);

	return value;
}

/*
 * request GDB interrupt
 */
static gboolean request_interrupt(void)
{
#ifdef DEBUG_OUTPUT
	char msg[1000];
	g_snprintf(msg, sizeof msg, "interrupting pid=%i", target_pid);
	dbg_cbs->send_message(msg, "red");
#endif

	requested_interrupt = TRUE;
	kill(target_pid, SIGINT);

	return TRUE;
}

/*
 * get GDB error messages
 */
static gchar* error_message(void)
{
	return err_message;
}

/*
 * define GDB debug module
 */
DBG_MODULE_DEFINE(gdb);


