/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
 * gtksourceundomanager.c
 * This file is part of GtkSourceView
 *
 * Copyright (C) 1998, 1999 Alex Roberts, Evan Lawrence
 * Copyright (C) 2000, 2001 Chema Celorio, Paolo Maggi
 * Copyright (C) 2002-2005  Paolo Maggi
 *
 * 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 02111-1301, USA.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <glib.h>
#include <stdlib.h>
#include <string.h>

#include "gtksourceundomanager.h"
#include "gtksourceview-marshal.h"


#define DEFAULT_MAX_UNDO_LEVELS		25


typedef struct _GtkSourceUndoAction  			GtkSourceUndoAction;
typedef struct _GtkSourceUndoInsertAction		GtkSourceUndoInsertAction;
typedef struct _GtkSourceUndoDeleteAction		GtkSourceUndoDeleteAction;
typedef struct _GtkSourceUndoInsertAnchorAction GtkSourceUndoInsertAnchorAction;

typedef enum {
	GTK_SOURCE_UNDO_ACTION_INSERT,
	GTK_SOURCE_UNDO_ACTION_DELETE,
	GTK_SOURCE_UNDO_ACTION_INSERT_ANCHOR,
} GtkSourceUndoActionType;

/*
 * We use offsets instead of GtkTextIters because the last ones
 * require to much memory in this context without giving us any advantage.
 */

struct _GtkSourceUndoInsertAction
{
	gint   pos;
	gchar *text;
	gint   length;
	gint   chars;
};

struct _GtkSourceUndoDeleteAction
{
	gint   start;
	gint   end;
	gchar *text;
	gboolean forward;
};

struct _GtkSourceUndoInsertAnchorAction
{
	gint pos;
	GtkTextChildAnchor *anchor;
};

struct _GtkSourceUndoAction
{
	GtkSourceUndoActionType action_type;

	union {
		GtkSourceUndoInsertAction  insert;
		GtkSourceUndoDeleteAction  delete;
		GtkSourceUndoInsertAnchorAction insert_anchor;
	} action;

	gint order_in_group;

	/* It is TRUE whether the action can be merged with the following action. */
	guint mergeable : 1;

	/* It is TRUE whether the action is marked as "modified".
	 * An action is marked as "modified" if it changed the
	 * state of the buffer from "not modified" to "modified". Only the first
	 * action of a group can be marked as modified.
	 * There can be a single action marked as "modified" in the actions list.
	 */
	guint modified  : 1;
};

/* INVALID is a pointer to an invalid action */
#define INVALID ((void *) "IA")

struct _GtkSourceUndoManagerPrivate
{
	GtkTextBuffer	*document;

	GList*		 actions;
	gint 		 next_redo;

	gint 		 actions_in_current_group;

	gint		 running_not_undoable_actions;

	gint		 num_of_groups;

	gint		 max_undo_levels;

	guint	 	 can_undo : 1;
	guint		 can_redo : 1;

	/* It is TRUE whether, while undoing an action of the current group (with order_in_group > 1),
	 * the state of the buffer changed from "not modified" to "modified".
	 */
	guint	 	 modified_undoing_group : 1;

	/* Pointer to the action (in the action list) marked as "modified".
	 * It is NULL when no action is marked as "modified".
	 * It is INVALID when the action marked as "modified" has been removed
	 * from the action list (freeing the list or resizing it) */
	GtkSourceUndoAction *modified_action;
};

enum {
	CAN_UNDO,
	CAN_REDO,
	LAST_SIGNAL
};

static void gtk_source_undo_manager_class_init 			(GtkSourceUndoManagerClass 	*klass);
static void gtk_source_undo_manager_init 			(GtkSourceUndoManager 		*um);
static void gtk_source_undo_manager_finalize 			(GObject 			*object);

static void gtk_source_undo_manager_insert_text_handler 	(GtkTextBuffer 			*buffer,
							 	 GtkTextIter 			*pos,
		                             		 	 const 	gchar 			*text,
							 	 gint 				 length,
							 	 GtkSourceUndoManager 		*um);
static void gtk_source_undo_manager_insert_anchor_handler (GtkTextBuffer *buffer,
			                   GtkTextIter            *pos,
			                   GtkTextChildAnchor     *anchor,
			                   GtkSourceUndoManager   *um);
static void gtk_source_undo_manager_delete_range_handler 	(GtkTextBuffer 			*buffer,
							 	 GtkTextIter 			*start,
                        		      		 	 GtkTextIter 			*end,
							 	 GtkSourceUndoManager 		*um);
static void gtk_source_undo_manager_begin_user_action_handler 	(GtkTextBuffer 			*buffer,
								 GtkSourceUndoManager 		*um);
static void gtk_source_undo_manager_modified_changed_handler	(GtkTextBuffer                  *buffer,
								 GtkSourceUndoManager           *um);

static void gtk_source_undo_manager_free_action_list 		(GtkSourceUndoManager 		*um);

static void gtk_source_undo_manager_add_action 			(GtkSourceUndoManager 		*um,
		                                         	 const GtkSourceUndoAction 	*undo_action);
static void gtk_source_undo_manager_free_first_n_actions 	(GtkSourceUndoManager 		*um,
								 gint 				 n);
static void gtk_source_undo_manager_check_list_size 		(GtkSourceUndoManager 		*um);

static gboolean gtk_source_undo_manager_merge_action 		(GtkSourceUndoManager 		*um,
		                                        	 const GtkSourceUndoAction 	*undo_action);

static GObjectClass 	*parent_class 				= NULL;
static guint 		undo_manager_signals [LAST_SIGNAL] 	= { 0 };

GType
gtk_source_undo_manager_get_type (void)
{
	static GType undo_manager_type = 0;

  	if (undo_manager_type == 0)
    	{
      		static const GTypeInfo our_info =
      		{
        		sizeof (GtkSourceUndoManagerClass),
        		NULL,		/* base_init */
        		NULL,		/* base_finalize */
        		(GClassInitFunc) gtk_source_undo_manager_class_init,
        		NULL,           /* class_finalize */
        		NULL,           /* class_data */
        		sizeof (GtkSourceUndoManager),
        		0,              /* n_preallocs */
        		(GInstanceInitFunc) gtk_source_undo_manager_init,
        		NULL		/* value_table */
      		};

      		undo_manager_type = g_type_register_static (G_TYPE_OBJECT,
                					    "GtkSourceUndoManager",
							    &our_info,
							    0);
    	}

	return undo_manager_type;
}

static void
gtk_source_undo_manager_class_init (GtkSourceUndoManagerClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);

  	parent_class = g_type_class_peek_parent (klass);

  	object_class->finalize = gtk_source_undo_manager_finalize;

        klass->can_undo 	= NULL;
	klass->can_redo 	= NULL;

	undo_manager_signals[CAN_UNDO] =
   		g_signal_new ("can_undo",
			      G_OBJECT_CLASS_TYPE (object_class),
			      G_SIGNAL_RUN_LAST,
			      G_STRUCT_OFFSET (GtkSourceUndoManagerClass, can_undo),
			      NULL, NULL,
			      gtksourceview_marshal_VOID__BOOLEAN,
			      G_TYPE_NONE,
			      1,
			      G_TYPE_BOOLEAN);

	undo_manager_signals[CAN_REDO] =
   		g_signal_new ("can_redo",
			      G_OBJECT_CLASS_TYPE (object_class),
			      G_SIGNAL_RUN_LAST,
			      G_STRUCT_OFFSET (GtkSourceUndoManagerClass, can_redo),
			      NULL, NULL,
			      gtksourceview_marshal_VOID__BOOLEAN,
			      G_TYPE_NONE,
			      1,
			      G_TYPE_BOOLEAN);
}

static void
gtk_source_undo_manager_init (GtkSourceUndoManager *um)
{
	um->priv = g_new0 (GtkSourceUndoManagerPrivate, 1);

	um->priv->actions = NULL;
	um->priv->next_redo = 0;

	um->priv->can_undo = FALSE;
	um->priv->can_redo = FALSE;

	um->priv->running_not_undoable_actions = 0;

	um->priv->num_of_groups = 0;

	um->priv->max_undo_levels = DEFAULT_MAX_UNDO_LEVELS;

	um->priv->modified_action = NULL;

	um->priv->modified_undoing_group = FALSE;
}

static void
gtk_source_undo_manager_finalize (GObject *object)
{
	GtkSourceUndoManager *um;

	g_return_if_fail (object != NULL);
	g_return_if_fail (GTK_SOURCE_IS_UNDO_MANAGER (object));

   	um = GTK_SOURCE_UNDO_MANAGER (object);

	g_return_if_fail (um->priv != NULL);

	if (um->priv->actions != NULL)
	{
		gtk_source_undo_manager_free_action_list (um);
	}

	g_signal_handlers_disconnect_by_func (G_OBJECT (um->priv->document),
			  G_CALLBACK (gtk_source_undo_manager_delete_range_handler),
			  um);

	g_signal_handlers_disconnect_by_func (G_OBJECT (um->priv->document),
			  G_CALLBACK (gtk_source_undo_manager_insert_text_handler),
			  um);

	g_signal_handlers_disconnect_by_func (G_OBJECT (um->priv->document),
			  G_CALLBACK (gtk_source_undo_manager_insert_anchor_handler),
			  um);

	g_signal_handlers_disconnect_by_func (G_OBJECT (um->priv->document),
			  G_CALLBACK (gtk_source_undo_manager_begin_user_action_handler),
			  um);

	g_free (um->priv);

	G_OBJECT_CLASS (parent_class)->finalize (object);
}

GtkSourceUndoManager*
gtk_source_undo_manager_new (GtkTextBuffer* buffer)
{
 	GtkSourceUndoManager *um;

	um = GTK_SOURCE_UNDO_MANAGER (g_object_new (GTK_SOURCE_TYPE_UNDO_MANAGER, NULL));

	g_return_val_if_fail (um->priv != NULL, NULL);
  	um->priv->document = buffer;

	g_signal_connect (G_OBJECT (buffer), "insert_text",
			  G_CALLBACK (gtk_source_undo_manager_insert_text_handler),
			  um);

	g_signal_connect (G_OBJECT (buffer), "insert_child_anchor",
			  G_CALLBACK (gtk_source_undo_manager_insert_anchor_handler),
			  um);

	g_signal_connect (G_OBJECT (buffer), "delete_range",
			  G_CALLBACK (gtk_source_undo_manager_delete_range_handler),
			  um);

	g_signal_connect (G_OBJECT (buffer), "begin_user_action",
			  G_CALLBACK (gtk_source_undo_manager_begin_user_action_handler),
			  um);

	g_signal_connect (G_OBJECT (buffer), "modified_changed",
			  G_CALLBACK (gtk_source_undo_manager_modified_changed_handler),
			  um);
	return um;
}

void
gtk_source_undo_manager_begin_not_undoable_action (GtkSourceUndoManager *um)
{
	g_return_if_fail (GTK_SOURCE_IS_UNDO_MANAGER (um));
	g_return_if_fail (um->priv != NULL);

	++um->priv->running_not_undoable_actions;
}

static void
gtk_source_undo_manager_end_not_undoable_action_internal (GtkSourceUndoManager *um)
{
	g_return_if_fail (GTK_SOURCE_IS_UNDO_MANAGER (um));
	g_return_if_fail (um->priv != NULL);

	g_return_if_fail (um->priv->running_not_undoable_actions > 0);

	--um->priv->running_not_undoable_actions;
}

void
gtk_source_undo_manager_end_not_undoable_action (GtkSourceUndoManager *um)
{
	g_return_if_fail (GTK_SOURCE_IS_UNDO_MANAGER (um));
	g_return_if_fail (um->priv != NULL);

	gtk_source_undo_manager_end_not_undoable_action_internal (um);

	if (um->priv->running_not_undoable_actions == 0)
	{
		gtk_source_undo_manager_free_action_list (um);

		um->priv->next_redo = -1;

		if (um->priv->can_undo)
		{
			um->priv->can_undo = FALSE;
			g_signal_emit (G_OBJECT (um),
				       undo_manager_signals [CAN_UNDO],
				       0,
				       FALSE);
		}

		if (um->priv->can_redo)
		{
			um->priv->can_redo = FALSE;
			g_signal_emit (G_OBJECT (um),
				       undo_manager_signals [CAN_REDO],
				       0,
				       FALSE);
		}
	}
}

gboolean
gtk_source_undo_manager_can_undo (const GtkSourceUndoManager *um)
{
	g_return_val_if_fail (GTK_SOURCE_IS_UNDO_MANAGER (um), FALSE);
	g_return_val_if_fail (um->priv != NULL, FALSE);

	return um->priv->can_undo;
}

gboolean
gtk_source_undo_manager_can_redo (const GtkSourceUndoManager *um)
{
	g_return_val_if_fail (GTK_SOURCE_IS_UNDO_MANAGER (um), FALSE);
	g_return_val_if_fail (um->priv != NULL, FALSE);

	return um->priv->can_redo;
}

static void
set_cursor (GtkTextBuffer *buffer, gint cursor)
{
	GtkTextIter iter;

	/* Place the cursor at the requested position */
	gtk_text_buffer_get_iter_at_offset (buffer, &iter, cursor);
	gtk_text_buffer_place_cursor (buffer, &iter);
}

static void
insert_text (GtkTextBuffer *buffer, gint pos, const gchar *text, gint len)
{
	GtkTextIter iter;

	gtk_text_buffer_get_iter_at_offset (buffer, &iter, pos);
	gtk_text_buffer_insert (buffer, &iter, text, len);
}

static void
insert_anchor (GtkTextBuffer *buffer, gint pos, GtkTextChildAnchor *anchor)
{
	GtkTextIter iter;

	gtk_text_buffer_get_iter_at_offset (buffer, &iter, pos);
	gtk_text_buffer_insert_child_anchor (buffer, &iter, anchor);
}

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

	gtk_text_buffer_get_iter_at_offset (buffer, &start_iter, start);

	if (end < 0)
		gtk_text_buffer_get_end_iter (buffer, &end_iter);
	else
		gtk_text_buffer_get_iter_at_offset (buffer, &end_iter, end);

	gtk_text_buffer_delete (buffer, &start_iter, &end_iter);
}

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

	gtk_text_buffer_get_iter_at_offset (buffer, &start_iter, start);

	if (end < 0)
		gtk_text_buffer_get_end_iter (buffer, &end_iter);
	else
		gtk_text_buffer_get_iter_at_offset (buffer, &end_iter, end);

	return gtk_text_buffer_get_slice (buffer, &start_iter, &end_iter, TRUE);
}

void
gtk_source_undo_manager_undo (GtkSourceUndoManager *um)
{
	GtkSourceUndoAction *undo_action;
	gboolean modified = FALSE;

	g_return_if_fail (GTK_SOURCE_IS_UNDO_MANAGER (um));
	g_return_if_fail (um->priv != NULL);
	g_return_if_fail (um->priv->can_undo);

	um->priv->modified_undoing_group = FALSE;

	gtk_source_undo_manager_begin_not_undoable_action (um);

	do
	{
		undo_action = g_list_nth_data (um->priv->actions, um->priv->next_redo + 1);
		g_return_if_fail (undo_action != NULL);

		/* undo_action->modified can be TRUE only if undo_action->order_in_group <= 1 */
		g_return_if_fail ((undo_action->order_in_group <= 1) ||
				  ((undo_action->order_in_group > 1) && !undo_action->modified));

		if (undo_action->order_in_group <= 1)
		{
			/* Set modified to TRUE only if the buffer did not change its state from
			 * "not modified" to "modified" undoing an action (with order_in_group > 1)
			 * in current group. */
			modified = (undo_action->modified && !um->priv->modified_undoing_group);
		}

		switch (undo_action->action_type)
		{
			case GTK_SOURCE_UNDO_ACTION_DELETE:
				insert_text (
					um->priv->document,
					undo_action->action.delete.start,
					undo_action->action.delete.text,
					strlen (undo_action->action.delete.text));

				if (undo_action->action.delete.forward)
					set_cursor (
						um->priv->document,
						undo_action->action.delete.start);
				else
					set_cursor (
						um->priv->document,
						undo_action->action.delete.end);

				break;

			case GTK_SOURCE_UNDO_ACTION_INSERT:
				delete_text (
					um->priv->document,
					undo_action->action.insert.pos,
					undo_action->action.insert.pos +
						undo_action->action.insert.chars);

				set_cursor (
					um->priv->document,
					undo_action->action.insert.pos);
				break;

			case GTK_SOURCE_UNDO_ACTION_INSERT_ANCHOR:
				delete_text (
					um->priv->document,
					undo_action->action.insert_anchor.pos,
					undo_action->action.insert_anchor.pos + 1);
				undo_action->action.insert_anchor.anchor->segment = NULL; /* XXX: This may be a bug in GTK+ */
				break;
			default:
				/* Unknown action type. */
				g_return_if_reached ();
		}

		++um->priv->next_redo;

	} while (undo_action->order_in_group > 1);

	if (modified)
	{
		--um->priv->next_redo;
		gtk_text_buffer_set_modified (um->priv->document, FALSE);
		++um->priv->next_redo;
	}

	gtk_source_undo_manager_end_not_undoable_action_internal (um);

	um->priv->modified_undoing_group = FALSE;

	if (!um->priv->can_redo)
	{
		um->priv->can_redo = TRUE;
		g_signal_emit (G_OBJECT (um),
			       undo_manager_signals [CAN_REDO],
			       0,
			       TRUE);
	}

	if (um->priv->next_redo >= (gint)(g_list_length (um->priv->actions) - 1))
	{
		um->priv->can_undo = FALSE;
		g_signal_emit (G_OBJECT (um),
			       undo_manager_signals [CAN_UNDO],
			       0,
			       FALSE);
	}
}

void
gtk_source_undo_manager_redo (GtkSourceUndoManager *um)
{
	GtkSourceUndoAction *undo_action;
	gboolean modified = FALSE;

	g_return_if_fail (GTK_SOURCE_IS_UNDO_MANAGER (um));
	g_return_if_fail (um->priv != NULL);
	g_return_if_fail (um->priv->can_redo);

	undo_action = g_list_nth_data (um->priv->actions, um->priv->next_redo);
	g_return_if_fail (undo_action != NULL);

	gtk_source_undo_manager_begin_not_undoable_action (um);

	do
	{
		if (undo_action->modified)
		{
			g_return_if_fail (undo_action->order_in_group <= 1);
			modified = TRUE;
		}

		--um->priv->next_redo;

		switch (undo_action->action_type)
		{
			case GTK_SOURCE_UNDO_ACTION_DELETE:
				delete_text (
					um->priv->document,
					undo_action->action.delete.start,
					undo_action->action.delete.end);

				set_cursor (
					um->priv->document,
					undo_action->action.delete.start);

				break;

			case GTK_SOURCE_UNDO_ACTION_INSERT:
				set_cursor (
					um->priv->document,
					undo_action->action.insert.pos);

				insert_text (
					um->priv->document,
					undo_action->action.insert.pos,
					undo_action->action.insert.text,
					undo_action->action.insert.length);

				break;

			case GTK_SOURCE_UNDO_ACTION_INSERT_ANCHOR:
				set_cursor (
					um->priv->document,
					undo_action->action.insert_anchor.pos);

				insert_anchor (
					um->priv->document,
					undo_action->action.insert_anchor.pos,
					undo_action->action.insert_anchor.anchor);
				break;

			default:
				/* Unknown action type */
				++um->priv->next_redo;
				g_return_if_reached ();
		}

		if (um->priv->next_redo < 0)
			undo_action = NULL;
		else
			undo_action = g_list_nth_data (um->priv->actions, um->priv->next_redo);

	} while ((undo_action != NULL) && (undo_action->order_in_group > 1));

	if (modified)
	{
		++um->priv->next_redo;
		gtk_text_buffer_set_modified (um->priv->document, FALSE);
		--um->priv->next_redo;
	}

	gtk_source_undo_manager_end_not_undoable_action_internal (um);

	if (um->priv->next_redo < 0)
	{
		um->priv->can_redo = FALSE;
		g_signal_emit (G_OBJECT (um), undo_manager_signals [CAN_REDO], 0, FALSE);
	}

	if (!um->priv->can_undo)
	{
		um->priv->can_undo = TRUE;
		g_signal_emit (G_OBJECT (um), undo_manager_signals [CAN_UNDO], 0, TRUE);
	}
}

static void
gtk_source_undo_action_free (GtkSourceUndoAction *action)
{
	if (action == NULL)
		return;

	if (action->action_type == GTK_SOURCE_UNDO_ACTION_INSERT)
		g_free (action->action.insert.text);
	else if (action->action_type == GTK_SOURCE_UNDO_ACTION_DELETE)
		g_free (action->action.delete.text);
	else if (action->action_type == GTK_SOURCE_UNDO_ACTION_INSERT_ANCHOR)
		g_object_unref(action->action.insert_anchor.anchor);
	else
		g_return_if_reached ();

	g_free (action);
}

static void
gtk_source_undo_manager_free_action_list (GtkSourceUndoManager *um)
{
	GList *l;

	l = um->priv->actions;

	while (l != NULL)
	{
		GtkSourceUndoAction *action = l->data;

		if (action->order_in_group == 1)
			--um->priv->num_of_groups;

		if (action->modified)
			um->priv->modified_action = INVALID;

		gtk_source_undo_action_free (action);

		l = g_list_next (l);
	}

	g_list_free (um->priv->actions);
	um->priv->actions = NULL;
}

static void
gtk_source_undo_manager_insert_text_handler (GtkTextBuffer 		*buffer,
					     GtkTextIter 		*pos,
		                             const gchar 		*text,
					     gint 			 length,
					     GtkSourceUndoManager 	*um)
{
	GtkSourceUndoAction undo_action;

	if (um->priv->running_not_undoable_actions > 0)
		return;

	undo_action.action_type = GTK_SOURCE_UNDO_ACTION_INSERT;

	undo_action.action.insert.pos    = gtk_text_iter_get_offset (pos);
	undo_action.action.insert.text   = (gchar*) text;
	undo_action.action.insert.length = length;
	undo_action.action.insert.chars  = g_utf8_strlen (text, length);

	if ((undo_action.action.insert.chars > 1) || (g_utf8_get_char (text) == '\n'))

	       	undo_action.mergeable = FALSE;
	else
		undo_action.mergeable = TRUE;

	undo_action.modified = FALSE;

	gtk_source_undo_manager_add_action (um, &undo_action);
}

static void gtk_source_undo_manager_insert_anchor_handler (GtkTextBuffer *buffer,
			                   GtkTextIter            *pos,
			                   GtkTextChildAnchor     *anchor,
			                   GtkSourceUndoManager   *um)
{
	GtkSourceUndoAction undo_action;

	if (um->priv->running_not_undoable_actions > 0)
		return;

	undo_action.action_type = GTK_SOURCE_UNDO_ACTION_INSERT_ANCHOR;

	undo_action.action.insert_anchor.pos    = gtk_text_iter_get_offset (pos);
	undo_action.action.insert_anchor.anchor = g_object_ref (anchor);

	undo_action.mergeable = FALSE;
	undo_action.modified = FALSE;

	gtk_source_undo_manager_add_action (um, &undo_action);
}

static void
gtk_source_undo_manager_delete_range_handler (GtkTextBuffer 		*buffer,
					      GtkTextIter 		*start,
                        		      GtkTextIter 		*end,
					      GtkSourceUndoManager 	*um)
{
	GtkSourceUndoAction undo_action;
	GtkTextIter insert_iter;

	if (um->priv->running_not_undoable_actions > 0)
		return;

	undo_action.action_type = GTK_SOURCE_UNDO_ACTION_DELETE;

	gtk_text_iter_order (start, end);

	undo_action.action.delete.start  = gtk_text_iter_get_offset (start);
	undo_action.action.delete.end    = gtk_text_iter_get_offset (end);

	undo_action.action.delete.text   = get_chars (
						buffer,
						undo_action.action.delete.start,
						undo_action.action.delete.end);

	/* figure out if the user used the Delete or the Backspace key */
	gtk_text_buffer_get_iter_at_mark (buffer, &insert_iter,
					  gtk_text_buffer_get_insert (buffer));
	if (gtk_text_iter_get_offset (&insert_iter) <= undo_action.action.delete.start)
		undo_action.action.delete.forward = TRUE;
	else
		undo_action.action.delete.forward = FALSE;

	if (((undo_action.action.delete.end - undo_action.action.delete.start) > 1) ||
	     (g_utf8_get_char (undo_action.action.delete.text  ) == '\n'))
	       	undo_action.mergeable = FALSE;
	else
		undo_action.mergeable = TRUE;

	undo_action.modified = FALSE;

	gtk_source_undo_manager_add_action (um, &undo_action);

	g_free (undo_action.action.delete.text);

}

static void
gtk_source_undo_manager_begin_user_action_handler (GtkTextBuffer *buffer, GtkSourceUndoManager *um)
{
	g_return_if_fail (GTK_SOURCE_IS_UNDO_MANAGER (um));
	g_return_if_fail (um->priv != NULL);

	if (um->priv->running_not_undoable_actions > 0)
		return;

	um->priv->actions_in_current_group = 0;
}

static void
gtk_source_undo_manager_add_action (GtkSourceUndoManager 	*um,
				    const GtkSourceUndoAction 	*undo_action)
{
	GtkSourceUndoAction* action;

	if (um->priv->next_redo >= 0)
	{
		gtk_source_undo_manager_free_first_n_actions (um, um->priv->next_redo + 1);
	}

	um->priv->next_redo = -1;

	if (!gtk_source_undo_manager_merge_action (um, undo_action))
	{
		action = g_new (GtkSourceUndoAction, 1);
		*action = *undo_action;

		if (action->action_type == GTK_SOURCE_UNDO_ACTION_INSERT)
			action->action.insert.text = g_strndup (undo_action->action.insert.text, undo_action->action.insert.length);
		else if (action->action_type == GTK_SOURCE_UNDO_ACTION_DELETE)
			action->action.delete.text = g_strdup (undo_action->action.delete.text);
		else if (action->action_type == GTK_SOURCE_UNDO_ACTION_INSERT_ANCHOR)
		{
			/* Nothing needs to be done */
		}
		else
		{
			g_free (action);
			g_return_if_reached ();
		}

		++um->priv->actions_in_current_group;
		action->order_in_group = um->priv->actions_in_current_group;

		if (action->order_in_group == 1)
			++um->priv->num_of_groups;

		um->priv->actions = g_list_prepend (um->priv->actions, action);
	}

	gtk_source_undo_manager_check_list_size (um);

	if (!um->priv->can_undo)
	{
		um->priv->can_undo = TRUE;
		g_signal_emit (G_OBJECT (um), undo_manager_signals [CAN_UNDO], 0, TRUE);
	}

	if (um->priv->can_redo)
	{
		um->priv->can_redo = FALSE;
		g_signal_emit (G_OBJECT (um), undo_manager_signals [CAN_REDO], 0, FALSE);
	}
}

static void
gtk_source_undo_manager_free_first_n_actions (GtkSourceUndoManager	*um,
					      gint 			 n)
{
	gint i;

	if (um->priv->actions == NULL)
		return;

	for (i = 0; i < n; i++)
	{
		GtkSourceUndoAction *action = g_list_first (um->priv->actions)->data;

		if (action->order_in_group == 1)
			--um->priv->num_of_groups;

		if (action->modified)
			um->priv->modified_action = INVALID;

		gtk_source_undo_action_free (action);

		um->priv->actions = g_list_delete_link (um->priv->actions,
							um->priv->actions);

		if (um->priv->actions == NULL)
			return;
	}
}

static void
gtk_source_undo_manager_check_list_size (GtkSourceUndoManager *um)
{
	gint undo_levels;

	g_return_if_fail (GTK_SOURCE_IS_UNDO_MANAGER (um));
	g_return_if_fail (um->priv != NULL);

	undo_levels = gtk_source_undo_manager_get_max_undo_levels (um);

	if (undo_levels < 1)
		return;

	if (um->priv->num_of_groups > undo_levels)
	{
		GtkSourceUndoAction *undo_action;
		GList *last;

		last = g_list_last (um->priv->actions);
		undo_action = (GtkSourceUndoAction*) last->data;

		do
		{
			GList *tmp;

			if (undo_action->order_in_group == 1)
				--um->priv->num_of_groups;

			if (undo_action->modified)
				um->priv->modified_action = INVALID;

			gtk_source_undo_action_free (undo_action);

			tmp = g_list_previous (last);
			um->priv->actions = g_list_delete_link (um->priv->actions, last);
			last = tmp;
			g_return_if_fail (last != NULL);

			undo_action = (GtkSourceUndoAction*) last->data;

		} while ((undo_action->order_in_group > 1) ||
			 (um->priv->num_of_groups > undo_levels));
	}
}

/**
 * gtk_source_undo_manager_merge_action:
 * @um: a #GtkSourceUndoManager.
 * @undo_action: a #GtkSourceUndoAction.
 *
 * This function tries to merge the undo action at the top of
 * the stack with a new undo action. So when we undo for example
 * typing, we can undo the whole word and not each letter by itself.
 *
 * Return Value: %TRUE is merge was successful, %FALSE otherwise.²
 **/
static gboolean
gtk_source_undo_manager_merge_action (GtkSourceUndoManager 	*um,
				      const GtkSourceUndoAction *undo_action)
{
	GtkSourceUndoAction *last_action;

	g_return_val_if_fail (GTK_SOURCE_IS_UNDO_MANAGER (um), FALSE);
	g_return_val_if_fail (um->priv != NULL, FALSE);

	if (um->priv->actions == NULL)
		return FALSE;

	last_action = (GtkSourceUndoAction*) g_list_nth_data (um->priv->actions, 0);

	if (!last_action->mergeable)
		return FALSE;

	if ((!undo_action->mergeable) ||
	    (undo_action->action_type != last_action->action_type))
	{
		last_action->mergeable = FALSE;
		return FALSE;
	}

	if (undo_action->action_type == GTK_SOURCE_UNDO_ACTION_DELETE)
	{
		if ((last_action->action.delete.forward != undo_action->action.delete.forward) ||
		    ((last_action->action.delete.start != undo_action->action.delete.start) &&
		     (last_action->action.delete.start != undo_action->action.delete.end)))
		{
			last_action->mergeable = FALSE;
			return FALSE;
		}

		if (last_action->action.delete.start == undo_action->action.delete.start)
		{
			gchar *str;

#define L  (last_action->action.delete.end - last_action->action.delete.start - 1)
#define g_utf8_get_char_at(p,i) g_utf8_get_char(g_utf8_offset_to_pointer((p),(i)))

			/* Deleted with the delete key */
			if ((g_utf8_get_char (undo_action->action.delete.text) != ' ') &&
			    (g_utf8_get_char (undo_action->action.delete.text) != '\t') &&
                            ((g_utf8_get_char_at (last_action->action.delete.text, L) == ' ') ||
			     (g_utf8_get_char_at (last_action->action.delete.text, L)  == '\t')))
			{
				last_action->mergeable = FALSE;
				return FALSE;
			}

			str = g_strdup_printf ("%s%s", last_action->action.delete.text,
				undo_action->action.delete.text);

			g_free (last_action->action.delete.text);
			last_action->action.delete.end += (undo_action->action.delete.end -
							   undo_action->action.delete.start);
			last_action->action.delete.text = str;
		}
		else
		{
			gchar *str;

			/* Deleted with the backspace key */
			if ((g_utf8_get_char (undo_action->action.delete.text) != ' ') &&
			    (g_utf8_get_char (undo_action->action.delete.text) != '\t') &&
                            ((g_utf8_get_char (last_action->action.delete.text) == ' ') ||
			     (g_utf8_get_char (last_action->action.delete.text) == '\t')))
			{
				last_action->mergeable = FALSE;
				return FALSE;
			}

			str = g_strdup_printf ("%s%s", undo_action->action.delete.text,
				last_action->action.delete.text);

			g_free (last_action->action.delete.text);
			last_action->action.delete.start = undo_action->action.delete.start;
			last_action->action.delete.text = str;
		}
	}
	else if (undo_action->action_type == GTK_SOURCE_UNDO_ACTION_INSERT)
	{
		gchar* str;

#define I (last_action->action.insert.chars - 1)

		if ((undo_action->action.insert.pos !=
		     	(last_action->action.insert.pos + last_action->action.insert.chars)) ||
		    ((g_utf8_get_char (undo_action->action.insert.text) != ' ') &&
		      (g_utf8_get_char (undo_action->action.insert.text) != '\t') &&
		     ((g_utf8_get_char_at (last_action->action.insert.text, I) == ' ') ||
		      (g_utf8_get_char_at (last_action->action.insert.text, I) == '\t')))
		   )
		{
			last_action->mergeable = FALSE;
			return FALSE;
		}

		str = g_strdup_printf ("%s%s", last_action->action.insert.text,
				undo_action->action.insert.text);

		g_free (last_action->action.insert.text);
		last_action->action.insert.length += undo_action->action.insert.length;
		last_action->action.insert.text = str;
		last_action->action.insert.chars += undo_action->action.insert.chars;

	}
	else if (undo_action->action_type == GTK_SOURCE_UNDO_ACTION_INSERT_ANCHOR)
	{
		/* Nothing needs to be done */
	}
	else
		/* Unknown action inside undo merge encountered */
		g_return_val_if_reached (TRUE);

	return TRUE;
}

gint
gtk_source_undo_manager_get_max_undo_levels (GtkSourceUndoManager *um)
{
	g_return_val_if_fail (um != NULL, 0);
	g_return_val_if_fail (GTK_SOURCE_IS_UNDO_MANAGER (um), 0);

	return um->priv->max_undo_levels;
}

void
gtk_source_undo_manager_set_max_undo_levels (GtkSourceUndoManager	*um,
				  	     gint			 max_undo_levels)
{
	gint old_levels;

	g_return_if_fail (um != NULL);
	g_return_if_fail (GTK_SOURCE_IS_UNDO_MANAGER (um));

	old_levels = um->priv->max_undo_levels;
	um->priv->max_undo_levels = max_undo_levels;

	if (max_undo_levels < 1)
		return;

	if (old_levels > max_undo_levels)
	{
		/* strip redo actions first */
		while (um->priv->next_redo >= 0 && (um->priv->num_of_groups > max_undo_levels))
		{
			gtk_source_undo_manager_free_first_n_actions (um, 1);
			um->priv->next_redo--;
		}

		/* now remove undo actions if necessary */
		gtk_source_undo_manager_check_list_size (um);

		/* emit "can_undo" and/or "can_redo" if appropiate */
		if (um->priv->next_redo < 0 && um->priv->can_redo)
		{
			um->priv->can_redo = FALSE;
			g_signal_emit (G_OBJECT (um), undo_manager_signals [CAN_REDO], 0, FALSE);
		}

		if (um->priv->can_undo &&
		    um->priv->next_redo >= (gint)(g_list_length (um->priv->actions) - 1))
		{
			um->priv->can_undo = FALSE;
			g_signal_emit (G_OBJECT (um), undo_manager_signals [CAN_UNDO], 0, FALSE);
		}
	}
}

static void
gtk_source_undo_manager_modified_changed_handler (GtkTextBuffer        *buffer,
                                            	  GtkSourceUndoManager *um)
{
	GtkSourceUndoAction *action;
	GList *list;

	g_return_if_fail (GTK_SOURCE_IS_UNDO_MANAGER (um));
	g_return_if_fail (um->priv != NULL);

	if (um->priv->actions == NULL)
		return;

	list = g_list_nth (um->priv->actions, um->priv->next_redo + 1);

	if (list != NULL)
		action = (GtkSourceUndoAction*) list->data;
	else
		action = NULL;

	if (gtk_text_buffer_get_modified (buffer) == FALSE)
	{
		if (action != NULL)
			action->mergeable = FALSE;

		if (um->priv->modified_action != NULL)
		{
			if (um->priv->modified_action != INVALID)
				um->priv->modified_action->modified = FALSE;

			um->priv->modified_action = NULL;
		}

		return;
	}

	if (action == NULL)
	{
		g_return_if_fail (um->priv->running_not_undoable_actions > 0);

		return;
	}

	/* gtk_text_buffer_get_modified (buffer) == TRUE */

	g_return_if_fail (um->priv->modified_action == NULL);

	if (action->order_in_group > 1)
		um->priv->modified_undoing_group  = TRUE;

	while (action->order_in_group > 1)
	{
		list = g_list_next (list);
		g_return_if_fail (list != NULL);

		action = (GtkSourceUndoAction*) list->data;
		g_return_if_fail (action != NULL);
	}

	action->modified = TRUE;
	um->priv->modified_action = action;
}
