/* 
 * Copyright (C) 1998 Andy C. Kahn <kahn@zk3.dec.com>
 * http://ack.netpedia.net/gnp/
 *
 * copied from gnotepad code, modified for use in Bluefish
 * modifications (C) 1999 Olivier Sessink olivier@lx.student.wau.nl
 *
 *     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., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
#include "default_include.h"
#include <string.h>

#include "bluefish.h"
#include "undo.h"
#include "document.h"

typedef struct {
	char *text;					/* text to be inserted or deleted */
	int start;					/* starts at this position */
	int end;					/* ends at this position */
	gint changed;				/* doc changed status at this undo node */
	undo_op_t op;				/* action to execute */
} undo_t;

/**********************************************************/
void redo_cb(GtkWidget * wgt, gpointer cbdata);
void undo_cb(GtkWidget * wgt, gpointer cbdata);
void undo_all_cb(GtkWidget * wgt, gpointer w);
void redo_all_cb(GtkWidget * wgt, gpointer w);
void undo_list_add(Tdocument * d, char *text, int start, int end, undo_op_t op);
static GList *undo_list_delete(GList * list);
static void undo_redo_common(gboolean which);

/**********************************************************/


/*
 * adds an undo item to the undolist.  if there exists items on the redo list,
 * removes them first.
 */
void undo_list_add(Tdocument * d, char *text, int start, int end, undo_op_t op)
{
	undo_t *undo;

#ifdef DEVELOPMENT
	g_assert(d);
	g_assert(text);
#endif /* DEVELOPMENT */
	DEBUG_MSG("undo_list_add, text=%p\n", text);
	DEBUG_MSG("undo_list_add, strlen(text)=%d, text=%s\n", strlen(text), text);
	if (strlen(text) == 0) {
		return;
	} else {
		undo = g_new(undo_t, 1);
		undo->text = text;
		undo->start = start;
		undo->end = end;
		undo->changed = d->modified;
		undo->op = op;
		DEBUG_MSG("undo_list_add, text=%s\n", text);

		/*
		 * if there's anything on the redo list, then we need to wipe out the
		 * redo list because we now have new undo items
		 */
		if (d->redolist) {
			d->redolist = undo_list_delete(d->redolist);
			d->redotail = NULL;
/*              tb_item_enable(d->w->main_tb, MAIN_TB_STR_REDO, FALSE); */
		}

		/*
		 * add to the undo list.  note that since it's conceivable that the
		 * undo list could get extremely long, appending to the list on each
		 * key typed by the user would result in awful response time.  so what
		 * we do is we keep track of the tail (end) of the list, and always
		 * append using the tail.
		 */
		if (!d->undolist) {
/*              tb_item_enable(d->w->main_tb, MAIN_TB_STR_UNDO, TRUE); */
			d->undolist = g_list_append(d->undolist, undo);
			d->undotail = d->undolist;
		} else {
			d->undotail = g_list_append(d->undotail, undo);
			d->undotail = g_list_next(d->undotail);
		}
	}
}								/* undo_list_add */


/*
 * used to remove either the undo list or the redo list.
 */
static GList *undo_list_delete(GList * list)
{
	GList *tmp;
	undo_t *undo;

	while (list) {
		tmp = list;
		list = g_list_remove_link(list, tmp);
#ifdef DEVELOPMENT
	g_assert(tmp->data);
#endif
		undo = (undo_t *) (tmp->data);
		g_list_free_1(tmp);
		g_free(undo->text);
		g_free(undo);
	}

	return list;
}								/* undo_list_delete */


/*
 * menu callback for the 'redo' command
 */
void redo_cb(GtkWidget * wgt, gpointer cbdata)
{
	undo_redo_common(1);
}								/* redo_cb */


/*
 * which: 0 = undo, 1 = redo
 */
static void undo_redo_common(gboolean which)
{
	Tdocument *d;
	undo_t *undo;
	GList *tmp;
	GList **srchead_cb, **srctail;
	GList **dsthead_cb, **dsttail;
	int pt;

	d = main_v->current_document;
	DEBUG_MSG("undo_redo_common, which=%d\n", which);

	/* FIXME:
	 * I don`t understand what is 8-)).
	 * You ask me why?
	 * See if below 8-() 
	 */
	g_assert(which == 0 || which == 1);

	/*
	 * we have two lists, the undo list, and the redo list.  here, figure
	 * out which one we're using, so that we know which list an item moves
	 * from, and which list it moves to.
	 */
	if (which == 0) {
		srchead_cb = &d->undolist;
		srctail = &d->undotail;
		dsthead_cb = &d->redolist;
		dsttail = &d->redotail;
	} else {
		srchead_cb = &d->redolist;
		srctail = &d->redotail;
		dsthead_cb = &d->undolist;
		dsttail = &d->undotail;
	}

	if (!(*srchead_cb) || !GTK_IS_TEXT(d->textbox))
		return;
	/*
	 * need to stop the signal, because if we add/delete text, it will
	 * generate the signal again!
	 */
	gtk_signal_disconnect(GTK_OBJECT(d->textbox), d->ins_txt_id);
	gtk_signal_disconnect(GTK_OBJECT(d->textbox), d->del_txt_id);
	gtk_text_freeze(GTK_TEXT(d->textbox));
	undo = (undo_t *) (*srctail)->data;
#ifdef DEVELOPMENT
	g_assert(undo->op == UndoInsert || undo->op == UndoDelete);
#endif
	gtk_text_set_point(GTK_TEXT(d->textbox), undo->start);

	/* TODO:
	 *  write wrapper for this code
	 */

	/* this seems to crash the GtkEditor widget ? */
	gtk_editable_set_position(GTK_EDITABLE(d->textbox), undo->start);


	DEBUG_MSG("undo_redo_common, point in text (or sctext) and editable set\n");

	if (which == 0) {			/* UNDO */
		if (undo->op == UndoInsert) {
			gtk_text_forward_delete(GTK_TEXT(d->textbox), undo->end - undo->start);
			DEBUG_MSG("undo_redo_common, text deleted\n");
		} else {
			gtk_text_insert(GTK_TEXT(d->textbox), NULL, NULL, NULL, undo->text, strlen(undo->text));
			pt = gtk_text_get_point(GTK_TEXT(d->textbox));
			DEBUG_MSG("undo_redo_common, text inserted A, about to set %d, gtk_text point %d\n", pt, gtk_text_get_length(GTK_TEXT(d->textbox)));
#ifdef DEVELOPMENT
			if (pt > gtk_text_get_length(GTK_TEXT(d->textbox))) {
				g_print("WARNING: pt > gtk_text_get_length(GTK_TEXT(d->textbox))\n");
			}
#endif
			gtk_editable_set_position(GTK_EDITABLE(d->textbox), pt);
		}

	} else {					/* REDO */
		if (undo->op == UndoInsert) {
			gtk_text_insert(GTK_TEXT(d->textbox), NULL, NULL, NULL, undo->text, strlen(undo->text));
			pt = gtk_text_get_point(GTK_TEXT(d->textbox));
			DEBUG_MSG("undo_redo_common, text inserted B, about to set %d, gtk_text point %d\n", pt, gtk_text_get_length(GTK_TEXT(d->textbox)));
#ifdef DEVELOPMENT
			if (pt > gtk_text_get_length(GTK_TEXT(d->textbox))) {
				g_print("WARNING: pt > gtk_text_get_length(GTK_TEXT(d->textbox))\n");
			}
#endif
			gtk_editable_set_position(GTK_EDITABLE(d->textbox), pt);


			DEBUG_MSG("undo_redo_common, editable set\n");
		} else {
#ifdef DEVELOPMENT
			if (undo->end > gtk_text_get_length(GTK_TEXT(d->textbox))) {
				g_print("WARNING: undo->end > gtk_text_get_length(GTK_TEXT(d->textbox))\n");
			}
#endif
			gtk_text_forward_delete(GTK_TEXT(d->textbox), undo->end - undo->start);
			DEBUG_MSG("undo_redo_common, text deleted\n");
		}
	}
	DEBUG_MSG("undo_redo_common,somewhere XXX\n");
	/* remove from src list, and place on dst list */
	tmp = *srctail;
	*srctail = g_list_previous(*srctail);
	*srchead_cb = g_list_remove_link(*srchead_cb, tmp);
	if (!(*dsthead_cb)) {
		*dsthead_cb = g_list_concat(*dsthead_cb, tmp);
		*dsttail = *dsthead_cb;
/*              tb_item_enable(d->w->main_tb,
   (which == 0) ?
   MAIN_TB_STR_REDO : MAIN_TB_STR_UNDO,
   TRUE); */
	} else {
		*dsttail = g_list_concat(*dsttail, tmp);
		*dsttail = g_list_next(*dsttail);
	}
	DEBUG_MSG("undo_redo_common,somewhere ZZZ\n");
	/* update the doc info label */
	if ((d->modified != undo->changed) || which == 1) {
		/* redo is always changed */
		doc_set_modified(d, which ? 1 : undo->changed);
		/* removed in favour of doc_set_modified() d->modified = which ? 1 : undo->changed; */
	}
	DEBUG_MSG("undo_redo_common, d->modified=%d\n", d->modified);
	/* now re-establish the signals */
	d->ins_txt_id = gtk_signal_connect(GTK_OBJECT(d->textbox), "insert_text", GTK_SIGNAL_FUNC(doc_insert_text_cb), d);
	d->del_txt_id = gtk_signal_connect(GTK_OBJECT(d->textbox), "delete_text", GTK_SIGNAL_FUNC(doc_delete_text_cb), d);
	gtk_text_thaw(GTK_TEXT(d->textbox));
	if (main_v->current_document->highlightstate) {
		doc_need_highlighting(main_v->current_document);
	}
}								/* undo_redo_common */


/*
 * PUBLIC: undo_cb
 *
 * menu callback for the 'undo' command
 */
void undo_cb(GtkWidget * wgt, gpointer cbdata)
{
	undo_redo_common(0);
}								/* undo_cb */


/*
 * PUBLIC: undo_all_cb
 *
 * menu callback for the 'undo all' command
 */
void undo_all_cb(GtkWidget * wgt, gpointer w)
{

	while (main_v->current_document->undolist)
		undo_redo_common(0);
}								/* undo_all_cb */


/*
 * PUBLIC: redo_all_cb
 *
 * menu callback for the 'redo all' command
 */
void redo_all_cb(GtkWidget * wgt, gpointer w)
{

	while (main_v->current_document->redolist)
		undo_redo_common(1);
}								/* redo_all_cb */
