#include <gtk/gtk.h>

#include "../include/string.h"

#include "editor.h"
#include "editorop.h"
#include "editorundo.h"

#include "manedit.h"


gpointer EditorUndoNew(
	editor_struct *editor, editor_undo_type type,
	const gchar *comment
);
void EditorUndoDelete(editor_struct *editor, gpointer u);

static editor_undo_insert_chars_struct *EditorUndoApplyRemove(
	editor_undo_remove_chars_struct *u_remove
);
static editor_undo_remove_chars_struct *EditorUndoApplyInsert(
	editor_undo_insert_chars_struct *u_insert
);

gint EditorUndoDoApply(
	editor_struct *editor,
	gpointer **in_list, gint *in_total,
	gpointer **out_list, gint *out_total,
	gint max
);


#define MAX(a,b)        (((a) > (b)) ? (a) : (b))
#define MIN(a,b)        (((a) < (b)) ? (a) : (b))
#define CLIP(a,l,h)     (MIN(MAX((a),(l)),(h)))


/*
 *	Allocates a new undo structure with the given type and initial
 *	values.
 */
gpointer EditorUndoNew(
	editor_struct *editor, editor_undo_type type,
	const gchar *comment
)
{
	gint len = 0;
	gpointer u = NULL;

	switch(type)
	{
	  case EDITOR_UNDO_REMOVE_CHARS:
	    len = sizeof(editor_undo_remove_chars_struct);
	    break;

	  case EDITOR_UNDO_INSERT_CHARS:
	    len = sizeof(editor_undo_insert_chars_struct);
	    break;
	}

	if(len > 0)
	{
	    u = g_malloc0(len);
	    if(u != NULL)
	    {
		editor_undo_common_struct *common = EDITOR_UNDO_COMMON(u);

		/* Set common member values */
		common->type = type;
		common->comment = (comment != NULL) ?
		    g_strdup(comment) : NULL;
		common->editor_ptr = editor;
	    }
	}

	return(u);
}

/*
 *	Deletes the undo structure and all its allocated resources.
 */
void EditorUndoDelete(editor_struct *editor, gpointer u)
{
	editor_undo_remove_chars_struct *u_remove;
	editor_undo_insert_chars_struct *u_insert;
 	editor_undo_common_struct *u_common;

	if(u == NULL)
	    return;

	/* Delete type specific data */
	switch(EDITOR_UNDO_COMMON(u)->type)
	{
	  case EDITOR_UNDO_REMOVE_CHARS:
	    u_remove = EDITOR_UNDO_REMOVE_CHARS(u);
	    break;

	  case EDITOR_UNDO_INSERT_CHARS:
	    u_insert = EDITOR_UNDO_INSERT_CHARS(u);
	    g_free(u_insert->characters);
	    break;
	}

	/* Delete common members */
	u_common = EDITOR_UNDO_COMMON(u);
	g_free(u_common->comment);

	g_free(u);
}



/*
 *	Procedure to remove characters (undo insert characters).
 *
 *	Returns an inverse insert characters operation or NULL on error.
 *
 *	Given input undo structure will be deleted and should not be
 * 	referenced again after this call.
 */
static editor_undo_insert_chars_struct *EditorUndoApplyRemove(
	editor_undo_remove_chars_struct *u_remove
)
{
	editor_undo_insert_chars_struct *u_insert = NULL;
	editor_struct *editor;
	GtkEditable *editable;
	GtkText *text;
	gchar *text_buf = NULL;


	if(u_remove == NULL)
	    return(u_insert);

	editor = EDITOR(u_remove->editor_ptr);
	editable = (GtkEditable *)u_remove->w;
	text = (GtkText *)u_remove->w;
	if((editor == NULL) || (editable == NULL))
	    return(u_insert);

	/* Get characters from editable that we're about to remove */
	if(u_remove->length > 0)
	    text_buf = gtk_editable_get_chars(
		editable,
		u_remove->position,
		u_remove->position + u_remove->length
	    );

	/* Create inverse operation structure and set values */
	u_insert = EDITOR_UNDO_INSERT_CHARS(EditorUndoNew(
	    editor, EDITOR_UNDO_INSERT_CHARS,
	    u_remove->comment
	));
	if(u_insert != NULL)
	{
	    u_insert->w = GTK_WIDGET(editable);

	    u_insert->position = u_remove->position;
	    u_insert->length = (text_buf != NULL) ?
		u_remove->length : 0;

	    g_free(u_insert->characters);
	    u_insert->characters = (text_buf != NULL) ?
		g_strdup(text_buf) : NULL;
	}
	g_free(text_buf);
	text_buf = NULL;

	/* Now remove the characters */
	if(use_text_delete)
	{
	    gtk_text_freeze(text);
	    gtk_text_set_point(text, (guint)u_remove->position);
	    gtk_text_forward_delete(text, (guint)u_remove->length);
	    gtk_text_thaw(text);
	}
	else
	{
	    gtk_text_freeze(text);
	    gtk_editable_delete_text(
		editable,
		u_remove->position,
		u_remove->position + u_remove->length
	    );
	    gtk_text_thaw(text);
	}

	/* Delete the input structure */
	EditorUndoDelete(editor, u_remove);
	u_remove = NULL;

	return(u_insert);
}

/*
 *      Procedure to insert characters (undo remove characters).
 *
 *      Returns an inverse remove characters operation or NULL on error.
 * 
 *      Given input undo structure will be deleted and should not be
 *      referenced again after this call.
 */
static editor_undo_remove_chars_struct *EditorUndoApplyInsert(
	editor_undo_insert_chars_struct *u_insert
)
{
	editor_undo_remove_chars_struct *u_remove = NULL;
	editor_struct *editor;
	GtkEditable *editable;


	if(u_insert == NULL)
	    return(u_remove);

	editor = EDITOR(u_insert->editor_ptr);
	editable = (GtkEditable *)u_insert->w;
	if((editor == NULL) || (editable == NULL))
	    return(u_remove);

	/* Insert characters */
	if((u_insert->length > 0) && (u_insert->characters != NULL))
	{
	    medit_core_struct *core_ptr = MEDIT_CORE(editor->core_ptr);
	    medit_styles_list_struct *styles_list = NULL;
	    GtkStyle *style_ptr = NULL;


	    if(core_ptr != NULL)
	    {
		styles_list = &core_ptr->styles_list;
		style_ptr = styles_list->edit_text_standard;
	    }

	    gtk_text_set_point(GTK_TEXT(editable), u_insert->position);

	    gtk_text_freeze(GTK_TEXT(editable));
	    gtk_text_insert(
		GTK_TEXT(editable),
		(style_ptr != NULL) ? style_ptr->font : NULL,
		NULL,		/* Foreground color */
		NULL,		/* Background color */
		u_insert->characters,
		u_insert->length
	    );
	    gtk_text_thaw(GTK_TEXT(editable));
	    gtk_editable_select_region(
		editable,
		u_insert->position,
		u_insert->position + u_insert->length
	    );
	    gtk_text_set_point(
		GTK_TEXT(editable),
		u_insert->position + u_insert->length
	    );

	    /* Need to perform syntax highlighting, because the insert
	     * callback is not called.
	     */
	    EditorSyntaxHighlight(
		editor, GTK_WIDGET(editable),
		u_insert->position, u_insert->position,
		u_insert->position + u_insert->length
	    );
	}

	/* Create inverse operation structure and set values */
	u_remove = EDITOR_UNDO_REMOVE_CHARS(EditorUndoNew(
	    editor, EDITOR_UNDO_REMOVE_CHARS,
	    u_insert->comment
	));
	if(u_remove != NULL)
	{
	    u_remove->w = GTK_WIDGET(editable);

	    u_remove->position = u_insert->position;
	    u_remove->length = u_insert->length;
	}

	/* Delete the input structure */
	EditorUndoDelete(editor, u_insert);
	u_insert = NULL;

	return(u_remove);
}


/*
 *	Procedure to apply the first undo item in the in_list and then
 *	then convert it into the inverse operation and transfer it to
 *	the out_list.
 *
 *	If highest item on out_list would exceed the given max then
 *	it will be deleted.
 *
 *	Returns non-zero on error.
 */
gint EditorUndoDoApply(
	editor_struct *editor,
	gpointer **in_list, gint *in_total,
	gpointer **out_list, gint *out_total,
	gint max
)
{
	gint i, n;
	gpointer u_in, u_inverse = NULL;


	if((editor == NULL) || (in_list == NULL) || (in_total == NULL))
	    return(-1);

	/* Nothing to do? */
	if((*in_list == NULL) || (*in_total < 1))
	    return(0);


	/* Get first undo operation from input list, newest undo
	 * operation is always the first in the list
	 */
	u_in = (*in_list)[0];

	/* Reduce and shift input list */
	*in_total = (*in_total) - 1;
	for(i = 0; i < *in_total; i++)
	    (*in_list)[i] = (*in_list)[i + 1];

	if(*in_total > 0)
	{
	    *in_list = (gpointer *)g_realloc(
		*in_list, (*in_total) * sizeof(gpointer)
	    );
	    if(*in_list == NULL)
		*in_total = 0;
	}
	else
	{
	    g_free(*in_list);
	    *in_list = NULL;
	    *in_total = 0;
	}



	/* Handle input undo operation by its type, note that the
	 * given input undo structure will be deleted
	 */
	switch(EDITOR_UNDO_COMMON(u_in)->type)
	{
	  case EDITOR_UNDO_REMOVE_CHARS:
	    u_inverse = EditorUndoApplyRemove(
		EDITOR_UNDO_REMOVE_CHARS(u_in)
	    );
	    u_in = NULL;
	    break;

	  case EDITOR_UNDO_INSERT_CHARS:
	    u_inverse = EditorUndoApplyInsert(
		EDITOR_UNDO_INSERT_CHARS(u_in)
	    );
	    u_in = NULL;
	    break;
	}

	/* Undo not processed? */
	if(u_in != NULL)
	{
	    EditorUndoDelete(editor, u_in);
	    u_in = NULL;
	}



	/* Did we get an inverse operation? */
	if(u_inverse != NULL)
	{
	    /* Add it to the out list */
	    if((out_list != NULL) && (out_total != NULL))
	    {
		if(*out_total < 0)
		   *out_total = 0;

		*out_total = (*out_total) + 1;
		*out_list = (gpointer *)g_realloc(
		    *out_list,
		    (*out_total) * sizeof(gpointer)
		);
		if(*out_list == NULL)
		{
		    *out_total = 0;

		    EditorUndoDelete(editor, u_inverse);
		    u_inverse = NULL;
		}
		else
		{
		    /* Shift */
		    for(i = (*out_total) - 1; i > 0; i--)
			(*out_list)[i] = (*out_list)[i - 1];

		    /* Record newest undo inverse structure as first item */
		    (*out_list)[0] = u_inverse;
		    u_inverse = NULL;
		}

		/* Delete older undo structures on the out list */
		if(*out_total > max)
		{
		    n = MAX(max, 0);

		    /* Delete all undo structures greater than the max */
		    for(i = (*out_total) - 1; i >= n; i--)
			EditorUndoDelete(editor, (*out_list)[i]);

		    (*out_total) = n;
		    if((*out_total) > 0)
		    {
			(*out_list) = (gpointer*)g_realloc(
			    *out_list,
			    (*out_total) * sizeof(gpointer)
			);
			if((*out_list) == NULL)
			    (*out_total) = 0;
		    }
		    else
		    {
			g_free(*out_list);
			(*out_list) = NULL;
			(*out_total) = 0;
		    }
		}
	    }
	    else
	    {
		/* No out list available, delete inverse operation
		 * structure.
		 */
		EditorUndoDelete(editor, u_inverse);
		u_inverse = NULL;
	    }
	}

	return(0);
}
