/*
 * mape - C4 Landscape.txt editor
 *
 * Copyright (c) 2005-2009, Armin Burgmeier
 *
 * Distributed under the terms of the ISC license; see accompanying file
 * "COPYING" for details.
 *
 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
 * See accompanying file "TRADEMARK" for details.
 *
 * To redistribute this file separately, substitute the full license texts
 * for the above references.
 */

#include <stdlib.h>
#include <gtksourceview/gtksourceview.h>
#include <gtksourceview/gtksourcebuffer.h>
#include "mape/mapgen.h"
#include "mape/random.h"
#include "mape/preferences.h"
#include "mape/mattexview.h"
#include "mape/editview.h"
#include "mape/statusbar.h"
#include "mape/preview.h"

typedef struct _ThreadData ThreadData;
struct _ThreadData {
	MapeEditView* view;
	gchar* source;
	MapeMapgenType type;
	gchar* file_path;
	MapeMaterialMap* mat_map;
	MapeTextureMap* tex_map;
	guint map_width;
	guint map_height;
	gdouble map_zoom;
	gint64 start_time;
};

typedef struct _ThreadResult ThreadResult;
struct _ThreadResult {
	MapeEditView* view;
	GdkPixbuf* pixbuf;
	GError* error;
	guint idle_id;
	gint64 start_time;
};

static void mape_edit_view_set_filename(MapeEditView* view,
                                        const gchar* filename)
{
	MapeMapgenType new_type;
	GtkSourceBuffer* buf;

	gchar* basename;
	gchar* utf8_file;
	gchar* utf8_file_casefold;
	gchar* utf8_landscape_txt_casefold;

	const gchar* lang_name;
	const gchar* lang_filename;
	GtkSourceLanguage* lang;
	GtkSourceStyleScheme* style;
	GtkWidget* error_dialog;

	buf = GTK_SOURCE_BUFFER(
		gtk_text_view_get_buffer(GTK_TEXT_VIEW(view->view) )
	);

	if (view->file_path != filename)
	{
		if(filename != NULL)
		{
			view->file_path = g_strdup(filename);
		}
		else
		{
			view->file_path = NULL;
		}
	}

	if(filename != NULL)
	{
		basename = g_path_get_basename(filename);

		/* Determine document type */
		utf8_file = g_filename_to_utf8(
			basename,
			-1,
			NULL,
			NULL,
			NULL
		);

		g_free(basename);

		if(utf8_file != NULL)
		{
			utf8_file_casefold = g_utf8_casefold(utf8_file, -1);
			utf8_landscape_txt_casefold = g_utf8_casefold("landscape.txt", -1);
			g_free(utf8_file);

			if(g_utf8_collate(utf8_file_casefold, utf8_landscape_txt_casefold) == 0)
				new_type = MAPE_MAPGEN_LANDSCAPE_TXT;
			else
				new_type = MAPE_MAPGEN_MAP_C;
			g_free(utf8_file_casefold);
			g_free(utf8_landscape_txt_casefold);
		}
	}
	else
	{
		new_type = MAPE_MAPGEN_MAP_C;
	}

	if(new_type != view->type)
	{
		view->type = new_type;
		switch(view->type)
		{
		case MAPE_MAPGEN_LANDSCAPE_TXT:
			lang_name = "c4landscape";
			lang_filename = "Landscape.txt";
			break;
		case MAPE_MAPGEN_MAP_C:
			lang_name = "c4mapscript";
			lang_filename = "Map.c";
			break;
		default:
			lang_name = NULL;
			g_assert_not_reached();
			break;
		}

		/* Set language according to document type */
		lang = gtk_source_language_manager_get_language(
			view->lang_manager,
			lang_name
		);

		style = gtk_source_style_scheme_manager_get_scheme(
			view->style_manager,
			"mape"
		);

		if(lang == NULL || style == NULL)
		{
			/* TODO: Show location where we search in */
			error_dialog = gtk_message_dialog_new(
				NULL,
				GTK_DIALOG_MODAL,
				GTK_MESSAGE_ERROR,
				GTK_BUTTONS_OK,
				"Syntax Highlighting Not Available"
			);

			gtk_message_dialog_format_secondary_text(
				GTK_MESSAGE_DIALOG(error_dialog),
				"The syntax highlighting file for %s files "
				"could not be located. Perhaps mape has not "
				"been properly installed. Syntax "
				"highlighting is disabled.",
				lang_filename
			);

			gtk_window_set_title(GTK_WINDOW(error_dialog), "Mape");

			gtk_dialog_run(GTK_DIALOG(error_dialog) );
			gtk_widget_destroy(error_dialog);
		}
		else
		{
			gtk_source_buffer_set_language(buf, lang);
			gtk_source_buffer_set_style_scheme(buf, style);
		}
	}
}

static void
mape_edit_view_thread_result_destroy_func(gpointer data)
{
	ThreadResult* result = (ThreadResult*)data;
	if(result->pixbuf != NULL) g_object_unref(G_OBJECT(result->pixbuf));
	if(result->error != NULL) g_error_free(result->error);
	/* Don't need to g_source_remove(result->idle_id) since this is
	 * only called when the idle handler is removed anyway. */
}

static void mape_edit_view_cb_changed(GtkTextBuffer* buffer,
                                      gpointer user_data)
{
	mape_edit_view_reload((MapeEditView*)user_data);
}

static void mape_edit_view_cb_update(GtkWidget* widget,
                                     GdkEvent* event,
                                     gpointer user_data)
{
	mape_edit_view_reload((MapeEditView*)user_data);
}

static GdkPixbuf* mape_edit_view_render_map(const gchar* source,
                                            MapeMapgenType type,
                                            const gchar* file_path,
                                            MapeMaterialMap* mat_map,
                                            MapeTextureMap* tex_map,
                                            guint map_width,
                                            guint map_height,
					    gdouble map_zoom,
                                            GError** error)
{
	GdkPixbuf* pixbuf;
	gchar* basename;
	gchar* dirname;
	gchar* scriptname;
	const gchar* filename;

	guint zoom_width;
	guint zoom_height;
	GdkPixbuf* zoombuf;

	if(mat_map == NULL || tex_map == NULL)
	{
		g_set_error(
			error,
			g_quark_from_static_string("MAPE_EDIT_VIEW_ERROR"),
			MAPE_EDIT_VIEW_ERROR_MISSING_MAPS,
			"Material.ocg is not loaded"
		);

		return NULL;
	}

	if(file_path != NULL)
	{
		basename = g_path_get_basename(file_path);
		filename = basename;

		dirname = g_path_get_dirname(file_path);
		scriptname = g_build_filename(dirname, "Script.c", NULL);
		g_free(dirname);
	}
	else
	{
		basename = NULL;
		scriptname = NULL;
		switch(type)
		{
		case MAPE_MAPGEN_LANDSCAPE_TXT:
			filename = "Landscape.txt";
			break;
		case MAPE_MAPGEN_MAP_C:
			filename = "Map.c";
			break;
		default:
			filename = NULL;
			g_assert_not_reached();
			break;
		}
	}

	pixbuf = mape_mapgen_render(
		filename,
		source,
		type,
		scriptname,
		mat_map,
		tex_map,
		map_width,
		map_height,
		error
	);

	/* Zoom image to output size */
	if(pixbuf != NULL)
	{
		map_width = gdk_pixbuf_get_width(pixbuf);
		map_height = gdk_pixbuf_get_height(pixbuf);

		zoom_width = (guint)(map_width * map_zoom + 0.5);
		zoom_height = (guint)(map_height * map_zoom + 0.5);
		if(map_width != zoom_width || map_height != zoom_height)
		{
			zoombuf = gdk_pixbuf_scale_simple(
				pixbuf,
				zoom_width,
				zoom_height,
				GDK_INTERP_BILINEAR
			);

			g_object_unref(pixbuf);
			pixbuf = zoombuf;
		}
	}

	g_free(basename);
	return pixbuf;
}

static gboolean
mape_edit_view_thread_result(gpointer data_)
{
	ThreadResult* result;
	MapeEditView* view;

	gint64 now;
	gchar* time_text;

	result = (ThreadResult*)data_;
	view = result->view;

	if(result->pixbuf == NULL)
	{
		g_assert(result->error != NULL);

		mape_statusbar_set_compile(
			view->statusbar,
			result->error->message
		);
	}
	else
	{
		now = g_get_monotonic_time();
		g_assert(now >= result->start_time);

		time_text = g_strdup_printf(
			"Landscape rendered successfully (%.2fs)",
			(now - result->start_time) / 1000000.
		);

		mape_statusbar_set_compile(view->statusbar, time_text);
		g_free(time_text);
	}

	mape_pre_view_update(view->pre_view, result->pixbuf);

	/* Finish thread */
	g_thread_join(view->render_thread);
	view->render_thread = NULL;

	/* Do we have to render again (someone changed
	   the source while rendering)? */
	if(view->rerender == TRUE)
		mape_edit_view_reload(view);

	return FALSE;
}

static gpointer mape_edit_view_thread_entry(gpointer data_)
{
	ThreadData* data;
	ThreadResult* result;
	GError* error = NULL;
	GdkPixbuf* res_buf;

	data = (ThreadData*)data_;

	res_buf = mape_edit_view_render_map(
		data->source,
		data->type,
		data->file_path,
		data->mat_map,
		data->tex_map,
		data->map_width,
		data->map_height,
		data->map_zoom,
		&error
	);

	result = g_slice_new(ThreadResult);
	result->view = data->view;
	result->pixbuf = res_buf;
	result->error = error;
	result->start_time = data->start_time;

	g_free(data->source);
	g_free(data->file_path);
	g_slice_free(ThreadData, data);

	result->idle_id = g_idle_add_full(
		G_PRIORITY_DEFAULT_IDLE,
		mape_edit_view_thread_result,
		result,
		mape_edit_view_thread_result_destroy_func
	);

	/* Note that you can only use this securly with g_thread_join,
	 * otherwise the idle handler might already have been run */
	return result;
}

MapeEditView* mape_edit_view_new(MapePreView* pre_view,
                                 MapeStatusbar* statusbar,
                                 GError** error)
{
	MapeEditView* view;
	GtkSourceBuffer* buf;
	GPtrArray* search_dirs;
	const gchar* const* data_dirs;
	const gchar* const* dir;

	view = malloc(sizeof(MapeEditView) );
	view->pre_view = pre_view;
	view->statusbar = statusbar;
	view->file_path = NULL;
	view->encoding = "UTF-8";
	view->type = MAPE_MAPGEN_NONE;
	view->render_thread = NULL;
	view->rerender = FALSE;
	view->fixed_seed = FALSE;

	view->view = gtk_source_view_new();
	buf = GTK_SOURCE_BUFFER(
		gtk_text_view_get_buffer(GTK_TEXT_VIEW(view->view) )
	);

	g_signal_connect_after(
		G_OBJECT(buf),
		"changed",
		G_CALLBACK(mape_edit_view_cb_changed),
		view
	);

	g_signal_connect(
		G_OBJECT(pre_view->event_box),
		"button-press-event",
		G_CALLBACK(mape_edit_view_cb_update),
		view
	);

	view->font_desc = pango_font_description_new();
	pango_font_description_set_family(view->font_desc, "monospace");
	gtk_widget_modify_font(view->view, view->font_desc);

	search_dirs = g_ptr_array_new();
#ifdef G_OS_WIN32
	g_ptr_array_add(
		search_dirs,
		g_win32_get_package_installation_subdirectory(NULL, NULL, "mape-syntax")
	);
#endif

	g_ptr_array_add(
		search_dirs,
		g_build_filename(g_get_home_dir(), ".mape-syntax", NULL)
	);

	g_ptr_array_add(
		search_dirs,
		g_strdup("./mape-syntax")
	);

	g_ptr_array_add(
		search_dirs,
		g_strdup("./src/mape/mape-syntax")
	);

	data_dirs = g_get_system_data_dirs();
	for(dir = data_dirs; *dir != NULL; ++ dir)
		g_ptr_array_add(search_dirs, g_build_filename(*dir, "mape", NULL));
	g_ptr_array_add(search_dirs, NULL);

	view->lang_manager = gtk_source_language_manager_new();
	gtk_source_language_manager_set_search_path(
		view->lang_manager,
		(gchar**)search_dirs->pdata
	);

	view->style_manager = gtk_source_style_scheme_manager_new();
	gtk_source_style_scheme_manager_set_search_path(
	  view->style_manager,
	  (gchar**)search_dirs->pdata
	);

	g_ptr_array_foreach(search_dirs, (GFunc)g_free, NULL);
	g_ptr_array_free(search_dirs, TRUE);

	mape_edit_view_set_filename(view, NULL);

	gtk_widget_show(view->view);
	
	view->window = gtk_scrolled_window_new(NULL, NULL);
	
	gtk_scrolled_window_set_policy(
		GTK_SCROLLED_WINDOW(view->window),
		GTK_POLICY_AUTOMATIC,
		GTK_POLICY_AUTOMATIC
	);
	
	gtk_scrolled_window_set_shadow_type(
		GTK_SCROLLED_WINDOW(view->window),
		GTK_SHADOW_IN
	);
	
	gtk_container_add(
		GTK_CONTAINER(view->window),
		view->view
	);

	gtk_widget_show(view->window);
	return view;
}

void mape_edit_view_destroy(MapeEditView* view)
{
	gpointer data;

	/* Wait for render thread to finish */
	if(view->render_thread != NULL)
	{
		data = g_thread_join(view->render_thread);
		view->render_thread = NULL;

		/* Don't let idle handler run since edit_view is about to be destroyed */
		g_assert(((ThreadResult*)data)->idle_id != 0);
		g_source_remove(((ThreadResult*)data)->idle_id);
	}

	g_object_unref(G_OBJECT(view->lang_manager) );
	g_object_unref(G_OBJECT(view->style_manager));
	g_free(view->file_path);
	free(view);
}

void mape_edit_view_clear(MapeEditView* view)
{
	g_free(view->file_path);
	view->file_path = NULL;

	/* TODO: Undoable action dingsen */
	/* (statische mape_edit_view_set_contents-Call?) */
	gtk_text_buffer_set_text(
		gtk_text_view_get_buffer(GTK_TEXT_VIEW(view->view)),
		"",
		0
	);
	
	gtk_text_buffer_set_modified(
		gtk_text_view_get_buffer(GTK_TEXT_VIEW(view->view)),
		FALSE
	);
}

gboolean mape_edit_view_open(MapeEditView* view,
                             const gchar* filename,
                             GError** error)
{
	gboolean result;
	gchar* contents;
	gchar* conv;
	gchar* utf8_file;
	gsize length;

	result = g_file_get_contents(filename, &contents, &length, error);
	if(result == FALSE) return FALSE;

	/* Assume UTF-8 */
	result = g_utf8_validate(contents, length, NULL);

	if(result == FALSE)
	{
		/* No UTF-8, try LATIN1 */
		conv = g_convert(
			contents,
			-1,
			"UTF-8",
			"LATIN1",
			NULL,
			NULL,
			NULL
		);

		g_free(contents);
		if(conv == NULL)
		{
			utf8_file = g_filename_to_utf8(
				filename,
				-1,
				NULL,
				NULL,
				NULL
			);

			if(utf8_file == NULL)
				utf8_file = g_strdup("<unknown file name>");

			g_set_error(
				error,
				g_quark_from_static_string(
					"MAPE_EDIT_VIEW_ERROR"
				),
				MAPE_EDIT_VIEW_ERROR_UNKNOWN_ENCODING,
				"Could not read file %s: Either the encoding "
				"is unknown or it is a binary file",
				utf8_file
			);

			g_free(utf8_file);
			return FALSE;
		}

		/* Conversion succeeded */
		contents = conv;
		view->encoding = "LATIN1";
	}
	else
	{
		view->encoding = "UTF-8";
	}

	mape_edit_view_set_filename(view, filename);

	/* TODO: Undoable action dingsen */
	/* (statische mape_edit_view_set_contents-Call?) */
	gtk_text_buffer_set_text(
		gtk_text_view_get_buffer(GTK_TEXT_VIEW(view->view)),
		contents,
		length
	);

	g_free(contents);

	gtk_text_buffer_set_modified(
		gtk_text_view_get_buffer(GTK_TEXT_VIEW(view->view)),
		FALSE
	);

	return TRUE;
}

gboolean mape_edit_view_save(MapeEditView* view,
                             const gchar* filename,
                             GError** error)
{
	GtkTextBuffer* buffer;
	GtkTextIter begin;
	GtkTextIter end;
	gchar* source;
	gchar* conv;
	gboolean result;

	buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(view->view) );
	gtk_text_buffer_get_start_iter(buffer, &begin);
	gtk_text_buffer_get_end_iter(buffer, &end);
	source = gtk_text_buffer_get_text(buffer, &begin, &end, TRUE);

	conv = g_convert(
		source,
		-1,
		view->encoding,
		"UTF-8",
		NULL,
		NULL,
		error
	);

	g_free(source);
	if(conv == NULL) return FALSE;

	result = g_file_set_contents(filename, conv, -1, error);
	g_free(conv);
	if(result == FALSE) return FALSE;
	
	gtk_text_buffer_set_modified(
		gtk_text_view_get_buffer(GTK_TEXT_VIEW(view->view)),
		FALSE
	);

	mape_edit_view_set_filename(view, filename);

	/* Rerender with new file path --
	 * different Script.c lookup for algo=script overlays */
	mape_edit_view_reload(view);

	return TRUE;
}

gboolean mape_edit_view_get_modified(MapeEditView* view)
{
	return gtk_text_buffer_get_modified(
		gtk_text_view_get_buffer(GTK_TEXT_VIEW(view->view))
	);
}

void mape_edit_view_undo(MapeEditView* edit_view)
{
	gtk_source_buffer_undo(
		GTK_SOURCE_BUFFER(
			gtk_text_view_get_buffer(GTK_TEXT_VIEW(edit_view->view))
		)
	);
}

void mape_edit_view_redo(MapeEditView* edit_view)
{
	gtk_source_buffer_redo(
		GTK_SOURCE_BUFFER(
			gtk_text_view_get_buffer(GTK_TEXT_VIEW(edit_view->view))
		)
	);
}

void mape_edit_view_apply_preferences(MapeEditView* edit_view,
                                      MapePreferences* preferences)
{
	GtkSourceView* view;
	GtkSourceBuffer* buffer;
	GtkWrapMode wrap_mode;
	
	view = GTK_SOURCE_VIEW(edit_view->view);
	buffer = GTK_SOURCE_BUFFER(
		gtk_text_view_get_buffer(GTK_TEXT_VIEW(view))
	);

	gtk_source_view_set_tab_width(
		view,
		preferences->tab_width
	);

	gtk_source_view_set_insert_spaces_instead_of_tabs(
		view,
		preferences->tab_to_spaces
	);

	gtk_source_view_set_auto_indent(
		view,
		preferences->auto_indentation
	);

	wrap_mode = GTK_WRAP_CHAR;
	if(preferences->text_wrapping == FALSE)
		wrap_mode = GTK_WRAP_NONE;
	gtk_text_view_set_wrap_mode(
		GTK_TEXT_VIEW(view),
		wrap_mode
	);
	
	gtk_source_view_set_show_line_numbers(
		view,
		preferences->line_numbers
	);

	gtk_source_view_set_highlight_current_line(
		view,
		preferences->highlight_line
	);
	
	gtk_source_buffer_set_highlight_matching_brackets(
		buffer,
		preferences->bracket_matching
	);

	edit_view->fixed_seed = preferences->fixed_seed;
	edit_view->random_seed = preferences->random_seed;
	edit_view->map_width = preferences->map_width;
	edit_view->map_height = preferences->map_height;
	edit_view->map_zoom = preferences->map_zoom;

	/* Rerender with new random settings */
	mape_edit_view_reload(edit_view);
}

void mape_edit_view_reload(MapeEditView* edit_view)
{
	GError* error = NULL;
	ThreadData* data;
	GtkTextBuffer* buffer;
	GtkTextIter begin;
	GtkTextIter end;

	if(edit_view->render_thread == NULL)
	{
		data = g_slice_new(ThreadData);

		buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(edit_view->view) );
		gtk_text_buffer_get_start_iter(buffer, &begin);
		gtk_text_buffer_get_end_iter(buffer, &end);

		/* TODO: We need to ref view so that it is guaranteed to be alive in the 
		 * thread result handler */
		data->view = edit_view;
		data->source = gtk_text_buffer_get_text(buffer, &begin, &end, TRUE);
		data->type = edit_view->type;
		data->file_path = g_strdup(edit_view->file_path);

		/* TODO: We need to ref these so noone can delete them while the thread
		 * uses them. */
		data->mat_map = edit_view->pre_view->mat_tex->mat_map,
		data->tex_map = edit_view->pre_view->mat_tex->tex_map,

		data->map_width = edit_view->map_width;
		data->map_height = edit_view->map_height;
		data->map_zoom = edit_view->map_zoom;
		data->start_time = g_get_monotonic_time();

		if(edit_view->fixed_seed == TRUE)
			mape_random_seed(edit_view->random_seed);

		mape_statusbar_set_compile(
			edit_view->statusbar,
			"Rendering map..."
		);

		edit_view->rerender = FALSE;

		edit_view->render_thread = g_thread_create(
			mape_edit_view_thread_entry,
			data,
			TRUE,
			&error
		);
		
		if(edit_view->render_thread == NULL)
		{
			mape_statusbar_set_compile(
				edit_view->statusbar,
				error->message
			);

			g_free(data->source);
			g_slice_free(ThreadData, data);

			g_error_free(error);
		}
	}
	else
	{
		/* Rerender when thread finished */
		edit_view->rerender = TRUE;
	}
}

