/*
 *  L3afpad - GTK+ based simple text editor
 *  Copyright (C) 2004-2005 Tarot Osuji
 *
 *  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 <gtk/gtk.h>
#include <gdk/gdkkeysyms.h>
#include <gdk/gdkkeysyms-compat.h>
#include "view.h"
#include "undo.h"

#define DV(x)

//"GTK_TEXT_VIEW(view)->overwrite_mode" can get overwrite_mode state

typedef struct {
	gchar command;
	gint start;
	gint end;
	gboolean seq; // sequency flag
	gchar *str;
} UndoInfo;

enum {
	INS = 0,
	BS,
	DEL
};

static GtkWidget *undo_w = NULL;
static GtkWidget *redo_w = NULL;
static GList *undo_list = NULL;
static GList *redo_list = NULL;
static GString *undo_gstr;
static UndoInfo *ui_tmp;
static guint modified_step;
static guint prev_keyval;
static gboolean seq_reserve = FALSE;

static void undo_flush_temporal_buffer(GtkTextBuffer *buffer);

static GList *undo_clear_info_list(GList *info_list)
{
	while (g_list_length(info_list)) {
		g_free(((UndoInfo *)info_list->data)->str);
		g_free(info_list->data);
		info_list = g_list_delete_link(info_list, info_list);
	}
	return info_list;
}

static void undo_append_undo_info(GtkTextBuffer *buffer, gchar command, gint start, gint end, gchar *str)
{
	UndoInfo *ui = g_malloc(sizeof(UndoInfo));

	ui->command = command;
	ui->start = start;
	ui->end = end;
//	ui->seq = FALSE;
	ui->seq = seq_reserve;
	ui->str = str;

	seq_reserve = FALSE;

	undo_list = g_list_append(undo_list, ui);
DV(g_print("undo_cb: %d %s (%d-%d)\n", command, str, start, end));
}

static void undo_create_undo_info(GtkTextBuffer *buffer, gchar command, gint start, gint end)
{
	GtkTextIter start_iter, end_iter;
	gboolean seq_flag = FALSE;
	gchar *str;
	gint keyval = get_current_keyval();

	gtk_text_buffer_get_iter_at_offset(buffer, &start_iter, start);
	gtk_text_buffer_get_iter_at_offset(buffer, &end_iter, end);
	str = gtk_text_buffer_get_text(buffer, &start_iter, &end_iter, FALSE);

	if (undo_gstr->len) {
		if ((end - start == 1) && (command == ui_tmp->command)) {
			switch (keyval) {
			case GDK_BackSpace:
				if (end == ui_tmp->start)
					seq_flag = TRUE;
				break;
			case GDK_Delete:
				if (start == ui_tmp->start)
					seq_flag = TRUE;
				break;
			case GDK_Tab:
			case GDK_space:
				if (start == ui_tmp->end)
					seq_flag = TRUE;
				break;
			default:
				if (start == ui_tmp->end)
					if (keyval && keyval < 0xF000)
						switch (prev_keyval) {
						case GDK_Return:
						case GDK_Tab:
						case GDK_space:
							break;
						default:
							seq_flag = TRUE;
						}
			}
		}
		if (seq_flag) {
			switch (command) {
			case BS:
				undo_gstr = g_string_prepend(undo_gstr, str);
				ui_tmp->start--;
				break;
			default:
				undo_gstr = g_string_append(undo_gstr, str);
				ui_tmp->end++;
			}
			redo_list = undo_clear_info_list(redo_list);
			prev_keyval = keyval;
			gtk_widget_set_sensitive(undo_w, TRUE);
			gtk_widget_set_sensitive(redo_w, FALSE);
			return;
		}
		undo_append_undo_info(buffer, ui_tmp->command, ui_tmp->start, ui_tmp->end, g_strdup(undo_gstr->str));
		undo_gstr = g_string_erase(undo_gstr, 0, -1);
	}

	if (!keyval && prev_keyval)
		undo_set_sequency(TRUE);

	if (end - start == 1 &&
		((keyval && keyval < 0xF000) ||
		  keyval == GDK_BackSpace || keyval == GDK_Delete || keyval == GDK_Tab)) {
		ui_tmp->command = command;
		ui_tmp->start = start;
		ui_tmp->end = end;
		undo_gstr = g_string_erase(undo_gstr, 0, -1);
		g_string_append(undo_gstr, str);
	} else
		undo_append_undo_info(buffer, command, start, end, g_strdup(str));

	redo_list = undo_clear_info_list(redo_list);
	prev_keyval = keyval;
	clear_current_keyval();
//	keyevent_setval(0);
	gtk_widget_set_sensitive(undo_w, TRUE);
	gtk_widget_set_sensitive(redo_w, FALSE);
}

static void cb_insert_text(GtkTextBuffer *buffer, GtkTextIter *iter, gchar *str)
{
	gint start, end;

DV(	g_print("insert-text\n"));
	end = gtk_text_iter_get_offset(iter);
	start = end - g_utf8_strlen(str, -1);

	undo_create_undo_info(buffer, INS, start, end);
}

static void cb_delete_range(GtkTextBuffer *buffer, GtkTextIter *start_iter, GtkTextIter *end_iter)
{
	gint start, end;
	gchar command;

DV(	g_print("delete-range\n"));
	start = gtk_text_iter_get_offset(start_iter);
	end = gtk_text_iter_get_offset(end_iter);

	if (get_current_keyval() == GDK_BackSpace)
		command = BS;
	else
		command = DEL;
	undo_create_undo_info(buffer, command, start, end);
}

void undo_reset_modified_step(GtkTextBuffer *buffer)
{
	undo_flush_temporal_buffer(buffer);
	modified_step = g_list_length(undo_list);
DV(g_print("undo_reset_modified_step: Reseted modified_step by %d\n", modified_step));
}

static void undo_check_modified_step(GtkTextBuffer *buffer)
{
	gboolean flag;

	flag = (modified_step == g_list_length(undo_list));
//g_print("%d - %d = %d\n", modified_step, g_list_length(undo_list), flag);
	if (gtk_text_buffer_get_modified(buffer) == flag)
		gtk_text_buffer_set_modified(buffer, !flag);
//g_print("change!\n");}
}
/*
static void undo_check_modified_step(GtkTextBuffer *buffer)
{
	if (modified_step == g_list_length(undo_list))
		gtk_text_buffer_set_modified(buffer, FALSE);
}
*//* // ????? "modified-changed" signal isn't emitted properly...
#include "window.h"
static void undo_check_modified_step(GtkTextBuffer *buffer)
{
	if (modified_step == g_list_length(undo_list)) {
		gtk_text_buffer_set_modified(buffer, FALSE);
		set_main_window_title();
	}
}
*/
static void cb_begin_user_action(GtkTextBuffer *buffer)
{
	g_signal_handlers_unblock_by_func(G_OBJECT(buffer),
		G_CALLBACK(cb_insert_text), NULL);
	g_signal_handlers_unblock_by_func(G_OBJECT(buffer),
		G_CALLBACK(cb_delete_range), NULL);
DV(g_print("begin-user-action(unblock_func)"));
DV(g_print(": keyval = 0x%X\n", get_current_keyval()));
}

static void cb_end_user_action(GtkTextBuffer *buffer)
{
	g_signal_handlers_block_by_func(G_OBJECT(buffer),
		G_CALLBACK(cb_insert_text), NULL);
	g_signal_handlers_block_by_func(G_OBJECT(buffer),
		G_CALLBACK(cb_delete_range), NULL);
DV(g_print("end-user-action(block_func)\n"));
}

void undo_clear_all(GtkTextBuffer *buffer)
{
	undo_list = undo_clear_info_list(undo_list);
	redo_list = undo_clear_info_list(redo_list);
	undo_reset_modified_step(buffer);
	gtk_widget_set_sensitive(undo_w, FALSE);
	gtk_widget_set_sensitive(redo_w, FALSE);

	ui_tmp->command = INS;
	undo_gstr = g_string_erase(undo_gstr, 0, -1);
	prev_keyval = 0;
}

void undo_init(GtkWidget *view, GtkWidget *undo_button, GtkWidget *redo_button)
{
	GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(view));

	undo_w = undo_button;
	redo_w = redo_button;

	g_signal_connect_after(G_OBJECT(buffer), "insert-text",
		G_CALLBACK(cb_insert_text), NULL);
	g_signal_connect(G_OBJECT(buffer), "delete-range",
		G_CALLBACK(cb_delete_range), NULL);
	g_signal_connect_after(G_OBJECT(buffer), "begin-user-action",
		G_CALLBACK(cb_begin_user_action), NULL);
	g_signal_connect(G_OBJECT(buffer), "end-user-action",
		G_CALLBACK(cb_end_user_action), NULL);
	cb_end_user_action(buffer);

	ui_tmp = g_malloc(sizeof(UndoInfo));
	undo_gstr = g_string_new("");

	undo_clear_all(buffer);
}

void undo_set_sequency(gboolean seq)
{
	if (g_list_length(undo_list))
		((UndoInfo *)g_list_last(undo_list)->data)->seq = seq;
DV(g_print("<undo_set_sequency: %d>\n", seq));
}

void undo_set_sequency_reserve(void)
{
	seq_reserve = TRUE;
}

static void undo_flush_temporal_buffer(GtkTextBuffer *buffer)
{
	if (undo_gstr->len) {
		undo_append_undo_info(buffer, ui_tmp->command,
			ui_tmp->start, ui_tmp->end, g_strdup(undo_gstr->str));
		undo_gstr = g_string_erase(undo_gstr, 0, -1);
	}
}

gboolean undo_undo_real(GtkTextBuffer *buffer)
{
	GtkTextIter start_iter, end_iter;
	UndoInfo *ui;

	undo_flush_temporal_buffer(buffer);
	if (g_list_length(undo_list)) {
//		undo_block_signal(buffer);
		ui = g_list_last(undo_list)->data;
		gtk_text_buffer_get_iter_at_offset(buffer, &start_iter, ui->start);
		switch (ui->command) {
		case INS:
			gtk_text_buffer_get_iter_at_offset(buffer, &end_iter, ui->end);
			gtk_text_buffer_delete(buffer, &start_iter, &end_iter);
			break;
		default:
			gtk_text_buffer_insert(buffer, &start_iter, ui->str, -1);
		}
		redo_list = g_list_append(redo_list, ui);
		undo_list = g_list_delete_link(undo_list, g_list_last(undo_list));
DV(g_print("cb_edit_undo: undo left = %d, redo left = %d\n",
g_list_length(undo_list), g_list_length(redo_list)));
//		undo_unblock_signal(buffer);
		if (g_list_length(undo_list)) {
			if (((UndoInfo *)g_list_last(undo_list)->data)->seq)
				return TRUE;
		} else
			gtk_widget_set_sensitive(undo_w, FALSE);
		gtk_widget_set_sensitive(redo_w, TRUE);
		if (ui->command == DEL)
			gtk_text_buffer_get_iter_at_offset(buffer, &start_iter, ui->start);
		gtk_text_buffer_place_cursor(buffer, &start_iter);
		scroll_to_cursor(buffer, 0.05);
	}
	undo_check_modified_step(buffer);
	return FALSE;
}

gboolean undo_redo_real(GtkTextBuffer *buffer)
{
	GtkTextIter start_iter, end_iter;
	UndoInfo *ri;

	if (g_list_length(redo_list)) {
//		undo_block_signal(buffer);
		ri = g_list_last(redo_list)->data;
		gtk_text_buffer_get_iter_at_offset(buffer, &start_iter, ri->start);
		switch (ri->command) {
		case INS:
			gtk_text_buffer_insert(buffer, &start_iter, ri->str, -1);
			break;
		default:
			gtk_text_buffer_get_iter_at_offset(buffer, &end_iter, ri->end);
			gtk_text_buffer_delete(buffer, &start_iter, &end_iter);
		}
		undo_list = g_list_append(undo_list, ri);
		redo_list = g_list_delete_link(redo_list, g_list_last(redo_list));
DV(g_print("cb_edit_redo: undo left = %d, redo left = %d\n",
g_list_length(undo_list), g_list_length(redo_list)));
//		undo_unblock_signal(buffer);
		if (ri->seq) {
			undo_set_sequency(TRUE);
			return TRUE;
		}
		if (!g_list_length(redo_list))
			gtk_widget_set_sensitive(redo_w, FALSE);
		gtk_widget_set_sensitive(undo_w, TRUE);
		gtk_text_buffer_place_cursor(buffer, &start_iter);
		scroll_to_cursor(buffer, 0.05);
	}
	undo_check_modified_step(buffer);
	return FALSE;
}

void undo_undo(GtkTextBuffer *buffer)
{
	while (undo_undo_real(buffer)) {};
}

void undo_redo(GtkTextBuffer *buffer)
{
	while (undo_redo_real(buffer)) {};
}
