#include <gtk/gtk.h>
#include <glib.h>
#include <stdint.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdbool.h>
#include <error-mem-viewer/err-mem-viewer-hex-cell-renderer.h>
#include <error-mem-viewer/safety-memory-types.h>
#include <error-mem-viewer/crc.h>

enum memory_view_columns {
	MEM_VIEW_COL_ADDRESS = 0,
	MEM_VIEW_COL_DATA,
	MEM_VIEW_COL_INTERPRETATION,
	MEM_VIEW_COLOR,
	MEM_VIEW_COLUMN_COUNT
};

struct application_data {
	GtkWidget *borrowed_main_window;
	GtkTreeView *borrowed_tree_view;
	GtkHeaderBar *borrowed_header_bar;
	GtkInfoBar *borrowed_info_bar;
	const char *error_memory_data;
	size_t file_size;
};

enum entry_state {
	ENTRY_STATE_VALID = 0,
	ENTRY_STATE_INVALID,
	ENTRY_STATE_NOT_PARSED,
	ENTRY_STATE_BLOCK
};

static void append_data(GtkTreeStore *store, GtkTreeIter *parent, GtkTreeIter *iter, GString *interpretation, enum entry_state state, uint32_t addr, uint32_t val)
{
	const GdkRGBA invalid_color = {
		.red = 1.0,
		.alpha = 1.0,
		.green = 0.0,
		.blue = 0.0,
	};

	const GdkRGBA valid_color = {
		.red = 0.0,
		.alpha = 1.0,
		.green = 1.0,
		.blue = 0.0,
	};

	const GdkRGBA warn_color = {
		.red = 1.0,
		.alpha = 1.0,
		.green = 1.0,
		.blue = 0.0,
	};

	const GdkRGBA block_color = {
		.red = 1.0,
		.alpha = 1.0,
		.green = 0.3,
		.blue = 0.5,
	};

	const GdkRGBA *col;

	switch (state) {
	case ENTRY_STATE_VALID:
		col = &valid_color;
		break;
	case ENTRY_STATE_NOT_PARSED:
		col = &warn_color;
		break;
	case ENTRY_STATE_BLOCK:
		col = &block_color;
		break;
	case ENTRY_STATE_INVALID:
	default:
		col = &invalid_color;
		break;
	}

	gtk_tree_store_append(store, iter, parent);
	gtk_tree_store_set(store, iter, MEM_VIEW_COL_ADDRESS, addr, MEM_VIEW_COLOR, col, MEM_VIEW_COL_INTERPRETATION, (interpretation ? interpretation->str : ""), MEM_VIEW_COL_DATA, val, -1);
	if (interpretation)
		g_string_free(interpretation, TRUE);
}

static void info_bar_show_message(GtkInfoBar *info_bar, GtkMessageType type, const char *format, ...)
{
	GString *string;
	va_list args;
	GtkWidget *content;
	GList *children, *iter;
	GtkWidget *label;

	va_start(args, format);
	string = g_string_new(NULL);

	g_string_vprintf(string, format, args);
	va_end(args);

	content = gtk_info_bar_get_content_area(info_bar);
	children = gtk_container_get_children(GTK_CONTAINER(content));
	for (iter = children; iter; iter = g_list_next(iter))
		gtk_widget_destroy(GTK_WIDGET(iter->data));
	g_list_free(children);

	label = gtk_label_new(string->str);
	gtk_container_add(GTK_CONTAINER(content), label);
	gtk_widget_show(label);
	gtk_info_bar_set_message_type(info_bar, type);
	gtk_widget_show(GTK_WIDGET(info_bar));
	gtk_info_bar_set_revealed(info_bar, TRUE);

	g_string_free(string, TRUE);
}

static bool check_err_mem_header(struct safety_memory_header *header, uint32_t *expected)
{
	uint32_t crc;

	crc = calculate_stm_crc((uint32_t *)header, sizeof(*header) / sizeof(uint32_t) - 1);
	if (expected)
		*expected = crc;
	if (crc == header->crc)
		return true;
	else
		return false;
}

static GString *new_string_printf(const char *fmt, ...)
{
	GString *string;
	va_list args;

	va_start(args, fmt);

	string = g_string_new(NULL);
	g_string_vprintf(string, fmt, args);

	va_end(args);

	return string;
}

static void show_error_memory(GtkTreeView *tree_view, const unsigned char *memory, size_t len)
{
	GtkTreeStore *store;
	GtkTreeIter iter;
	GtkTreeIter iter2;
	GtkTreeIter *parent_iter, *child_iter;
	enum entry_state state;
	GString *interpret;
	bool valid;
	bool analyze_further = true;
	struct safety_memory_header header;
	unsigned int i;
	uint32_t dat, expected_value;

	store = GTK_TREE_STORE(gtk_tree_view_get_model(tree_view));
	gtk_tree_store_clear(store);

	for (i = 0; i < len / 4; i++) {
		valid = false;
		interpret = NULL;
		dat = memory[i*4];
		dat |= ((uint32_t)memory[i*4+1]) << 8;
		dat |= ((uint32_t)memory[i*4+2]) << 16;
		dat |= ((uint32_t)memory[i*4+3]) << 24;

		state = ENTRY_STATE_NOT_PARSED;

		if (!analyze_further) {
			interpret = g_string_new("Not analyzed due to previous error");
			state = ENTRY_STATE_NOT_PARSED;
			parent_iter = NULL;
			child_iter = &iter;
			goto print;
		}
		/* Parse header */
		switch (i) {
		case 0: /* Magic */
			parent_iter = NULL;
			child_iter = &iter;
			append_data(store, parent_iter, child_iter, new_string_printf("Header"), ENTRY_STATE_BLOCK, i * 4, dat);
			parent_iter = child_iter;
			child_iter = &iter2;
			if (dat == SAFETY_MEMORY_MAGIC) {
				valid = true;
				interpret = g_string_new("Valid Header Magic");
			} else {
				interpret = g_string_new("Invalid Header Magic");
				analyze_further = false;
			}
			header.magic = dat;
			break;
		case 1:
			header.boot_status_offset = dat;

			valid = true;
			interpret = new_string_printf("Boot status offset addr: %u", dat);
			break;
		case 2:
			header.config_overrides_offset = dat;
			interpret = new_string_printf("Config override offset addr: %u", dat);
			valid = true;
			break;
		case 3:
			header.config_overrides_len = dat;
			interpret = new_string_printf("Config override length: %u", dat);
			valid = true;
			break;
		case 4:
			header.err_memory_offset = dat;
			interpret = new_string_printf("Error memory offset addr: %u", dat);
			valid = true;
			break;
		case 5:
			header.err_memory_end = dat;
			interpret = new_string_printf("Error memory end ptr: %u", dat);
			valid = true;
			break;
		case 6:
			header.crc = dat;
			valid = check_err_mem_header(&header, &expected_value);
			if (valid) {
				interpret = new_string_printf("Header CRC: 0x%08X", dat);
			} else {

				interpret = new_string_printf("Invalid CRC: 0x%08X | Expected: 0x%08X", dat, expected_value);
				analyze_further = false;
			}
			break;
		}
		if (i <= 6) {
			state = ENTRY_STATE_INVALID;
			goto print;
		}

		/* Check if we're in the boot status structure */
		if (i >= header.boot_status_offset && i < header.boot_status_offset + sizeof(struct safety_memory_boot_status) / 4) {
			if (i == header.boot_status_offset) {
				/* First entry of boot status struct */
				parent_iter = NULL;
				child_iter = &iter;
				interpret = new_string_printf("Boot Status Struct");
				append_data(store, parent_iter, child_iter, interpret, ENTRY_STATE_BLOCK, i * 4, dat);
				parent_iter = &iter;
				child_iter = &iter2;
				interpret = NULL;
			}
			switch (i - header.boot_status_offset) {
			case 0:
				interpret = new_string_printf("%s into bootloader", (dat ? "Boot" : "Do not boot"));
				valid = true;
				break;
			case 1:
				interpret = new_string_printf("Code %s been updated", (dat ? "has" : "hasn't"));
				valid = true;
				break;
			case 2:
				interpret = new_string_printf("Panic %s", (dat ? "occured" : "did not occur"));
				valid = true;
				break;
			default:
				valid = false;
				interpret = new_string_printf("Value undefined!");
				break;
			}
			state = ENTRY_STATE_INVALID;
			goto print;
		}

		if (i >= header.config_overrides_offset && i < (header.config_overrides_offset + header.config_overrides_len)) {
			if (i == header.config_overrides_offset) {
				parent_iter  = NULL;
				child_iter = &iter;
				append_data(store, parent_iter, child_iter,
					    new_string_printf("Config Overrides"),
					    ENTRY_STATE_BLOCK, i * 4, dat);
				parent_iter = child_iter;
				child_iter = &iter2;
			}

			interpret = new_string_printf("Config Overrides not yet implemented");
			valid = true;
			state = ENTRY_STATE_INVALID;
			goto print;
		}

		if (i >= header.err_memory_offset && i < header.err_memory_end) {
			if (i == header.err_memory_offset) {
				parent_iter  = NULL;
				child_iter = &iter;
				append_data(store, parent_iter, child_iter,
					    new_string_printf("Error Memory (length: %d)", header.err_memory_end - header.err_memory_offset),
					    ENTRY_STATE_BLOCK, i * 4, dat);
				parent_iter = child_iter;
				child_iter = &iter2;
			}

			switch (dat) {
			case SAFETY_MEMORY_NOP_ENTRY:
				valid = true;
				interpret = new_string_printf("Error Memory NOP");
				break;
			default:
				if ((dat & 0xFFU) == 0x51U) {
					valid = true;
					interpret = new_string_printf("Err memory Entry");
				} else
					interpret = new_string_printf("Invalid error memory entry");
				break;
			}
			state = ENTRY_STATE_INVALID;
			goto print;
		}

		if (i == header.err_memory_end) {
			expected_value = calculate_stm_crc((uint32_t *)memory, header.err_memory_end);
			if (expected_value == dat) {
				interpret = new_string_printf("Valid Memory CRC");
				valid = true;
			} else {
				interpret = new_string_printf("Invalid CRC. Expected: 0x%08X", expected_value);
			}
			state = ENTRY_STATE_INVALID;
			parent_iter = NULL;
			child_iter = &iter;
			goto print;
		}

		parent_iter = NULL;
		child_iter = &iter;
print:
		if (state != ENTRY_STATE_NOT_PARSED) {
			state = (valid ? ENTRY_STATE_VALID : ENTRY_STATE_INVALID);
		}
		append_data(store, parent_iter, child_iter, interpret, state, i * 4, dat);
	}


	if (!store)
		return;
}

void setup_tree_view(GtkTreeView *tree_view)
{
	GtkTreeStore *tree_store;
	GtkCellRenderer *string_renderer;
	GtkCellRenderer *hex_renderer;
	GtkTreeViewColumn *column;

	g_return_if_fail(GTK_IS_TREE_VIEW(tree_view));

	tree_store = gtk_tree_store_new(MEM_VIEW_COLUMN_COUNT, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_STRING, GDK_TYPE_RGBA);
	gtk_tree_view_set_model(tree_view, GTK_TREE_MODEL(tree_store));

	string_renderer = gtk_cell_renderer_text_new();
	hex_renderer = err_mem_view_hex_cell_renderer_new();

	column = gtk_tree_view_column_new_with_attributes("Address", hex_renderer, "hex-num", MEM_VIEW_COL_ADDRESS, "foreground-rgba", MEM_VIEW_COLOR, NULL);
	gtk_tree_view_append_column(tree_view, column);
	column = gtk_tree_view_column_new_with_attributes("Data", hex_renderer, "hex-num", MEM_VIEW_COL_DATA, "foreground-rgba", MEM_VIEW_COLOR, NULL);
	gtk_tree_view_append_column(tree_view, column);
	column = gtk_tree_view_column_new_with_attributes("Interpretation", string_renderer, "text", MEM_VIEW_COL_INTERPRETATION, "foreground-rgba", MEM_VIEW_COLOR, NULL);
	gtk_tree_view_append_column(tree_view, column);
}

static ptrdiff_t get_file_size(const char *filename)
{
	struct stat file_stat;
	int res;
	ptrdiff_t size;

	res = stat(filename, &file_stat);

	if (res)
		size = -1UL;
	else
		size = (ptrdiff_t)file_stat.st_size;

	return size;

}

/**
 * @brief Loads a file to memory and decodes it using base64
 * @param name Filename
 * @param detected_size detected Size of the ouput generated
 * @return Buffer of decoded data with size \p detected_size, NULL in case of an error.
 * @note Return buffer is allocated on heap. Use free() to free the memory.
 */
static char *load_file_to_memory(const char *name, size_t *detected_size)
{
	ptrdiff_t size;
	FILE *f;
	char *ret = NULL;
	size_t read_bytes;
	gsize decoded_size;

	size = get_file_size(name);
	if (size <= 0)
		goto exit;

	f = fopen(name, "rb");
	ret = (char *)malloc(size + 1);
	if (!ret)
		goto exit_close_file;

	read_bytes = fread(ret, 1UL, (size_t)size, f);
	if (read_bytes != (size_t)size) {
		free(ret);
		ret = NULL;
		goto exit_close_file;
	}

	ret[size] = '\0';

	g_base64_decode_inplace(ret, &decoded_size);
	if (decoded_size == 0) {
		free(ret);
		ret = NULL;
		goto exit_close_file;
	}

	if (detected_size)
		*detected_size = (size_t)decoded_size;
exit_close_file:
	fclose(f);
exit:
	return ret;
}

G_MODULE_EXPORT
void info_bar_close_cb(GtkInfoBar *info_bar, gpointer user_data)
{
	(void)user_data;

	gtk_info_bar_set_revealed(info_bar, FALSE);
}

G_MODULE_EXPORT
void open_button_clicked_cb(GtkButton *button, gpointer *user_data)
{
	struct application_data *data = (struct application_data *)user_data;
	(void)button;
	GtkDialog *dialog;
	gint res;
	gchar *filename;


	dialog = GTK_DIALOG(gtk_file_chooser_dialog_new("Open File", GTK_WINDOW(data->borrowed_main_window),
							GTK_FILE_CHOOSER_ACTION_OPEN,
							"Cancel", GTK_RESPONSE_CANCEL,
							"Open", GTK_RESPONSE_ACCEPT,
							NULL));
	res = gtk_dialog_run(dialog);
	if (res == GTK_RESPONSE_ACCEPT) {
		filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
		if (data->error_memory_data)
			free((void *)data->error_memory_data);
		data->error_memory_data = load_file_to_memory(filename, &data->file_size);
		if (!data->error_memory_data) {
			g_warning("File could not be loaded");
			g_free(filename);
			goto ret_destroy_dialog;
		}

		gtk_header_bar_set_subtitle(data->borrowed_header_bar, filename);
		g_free(filename);
	} else {
		goto ret_destroy_dialog;
	}

	if (data->file_size % 4 || data->file_size == 0) {
		free((void *)data->error_memory_data);
		data->error_memory_data = NULL;
		info_bar_show_message(data->borrowed_info_bar, GTK_MESSAGE_WARNING,
				      "Data must be base64 encoded and must contain full 32 bit words of data.");
		goto ret_destroy_dialog;
	}

	show_error_memory(data->borrowed_tree_view, (const unsigned char *)data->error_memory_data, data->file_size);

ret_destroy_dialog:
	gtk_widget_destroy(GTK_WIDGET(dialog));
}

static void app_activated(GApplication *app, gpointer user_data)
{
	struct application_data *data = (struct application_data *)user_data;
	GtkBuilder *builder;
	GtkWindow *main_window;

	builder = gtk_builder_new_from_resource("/gui/main");
	main_window = GTK_WINDOW(gtk_builder_get_object(builder, "main-window"));
	data->borrowed_main_window = GTK_WIDGET(main_window);
	data->error_memory_data = NULL;
	data->borrowed_tree_view = GTK_TREE_VIEW(gtk_builder_get_object(builder, "error-mem-tree-view"));
	data->borrowed_header_bar = GTK_HEADER_BAR(gtk_builder_get_object(builder, "header-bar"));
	data->borrowed_info_bar = GTK_INFO_BAR(gtk_builder_get_object(builder, "info-bar"));

	setup_tree_view(data->borrowed_tree_view);

	gtk_builder_connect_signals(builder, data);
	g_object_unref(builder);
	gtk_application_add_window(GTK_APPLICATION(app), main_window);
	gtk_widget_show(GTK_WIDGET(main_window));
}

static int start_gui(int argc, char **argv)
{
	int ret = 0;
	GtkApplication *g_app;
	static struct application_data data;

	/* Create a new G application which will start a completely new process for each call to the program (NON_UNIQUE) instead
	 * of creating an additional window in the same process
	 */
	g_app = gtk_application_new("de.shimatta.reflow.error-mem-viewer", G_APPLICATION_NON_UNIQUE);
	g_signal_connect(g_app, "activate", G_CALLBACK(app_activated), &data);
	ret = g_application_run(G_APPLICATION(g_app), argc, argv);
	g_object_unref(g_app);
	if (data.error_memory_data)
		free((void *)data.error_memory_data);

	return ret;
}

int main(int argc, char **argv)
{
	return start_gui(argc, argv);
}
