/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/* This file is part of the GtkHTML library
 *
 * Copyright (C) 2000 Helix Code, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHcANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public License
 * along with this library; see the file COPYING.LIB.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
*/

#include <config.h>
#include "htmlcursor.h"
#include "htmlengine.h"
#include "htmlundo.h"

struct _HTMLUndoStack {
	GList *stack;
	guint  size;
};
typedef struct _HTMLUndoStack HTMLUndoStack;

struct _HTMLUndo {
	HTMLUndoStack undo;
	HTMLUndoStack redo;
	HTMLUndoStack undo_used;

	/* these lists are stacks containing other
	 * levels undo/redo after calling html_undo_level_start */
	GSList   *undo_levels;
	GSList   *redo_levels;
	guint     level;
	guint     in_redo;
	gint      step_counter;

	gint      freeze_count; /* Freeze counter for im context */
};

#ifdef UNDO_DEBUG
static void html_undo_debug (HTMLUndo *undo);
#define DEBUG(x) html_undo_debug (x)
#else
#define DEBUG(x)
#endif

static void add_used_and_redo_to_undo (HTMLUndo *undo, HTMLEngine *engine);
static void level_destroy (HTMLUndoData *data);

inline static void
stack_copy (HTMLUndoStack *src,
            HTMLUndoStack *dst)
{
	dst->stack = src->stack;
	dst->size  = src->size;
}

inline static void
stack_dup (HTMLUndoStack *src,
           HTMLUndoStack *dst)
{
	dst->stack = g_list_copy (src->stack);
	dst->size  = src->size;
}

static void
destroy_action_list (GList *lp)
{
	GList *p;

	for (p = lp; p != NULL; p = p->next)
		html_undo_action_destroy (HTML_UNDO_ACTION (p->data));
}

static void
destroy_levels_list (GSList *lp)
{
	GSList *p;

	for (p = lp; p != NULL; p = p->next)
		level_destroy (p->data);
}


HTMLUndo *
html_undo_new (void)
{
	HTMLUndo *new_undo;

	new_undo = g_new0 (HTMLUndo, 1);

	return new_undo;
}

void
html_undo_destroy (HTMLUndo *undo)
{
	g_return_if_fail (undo != NULL);

	destroy_action_list (undo->undo.stack);
	destroy_action_list (undo->undo_used.stack);
	destroy_action_list (undo->redo.stack);

	g_list_free (undo->undo.stack);
	g_list_free (undo->undo_used.stack);
	g_list_free (undo->redo.stack);

	destroy_levels_list (undo->undo_levels);
	destroy_levels_list (undo->redo_levels);

	g_slist_free (undo->undo_levels);
	g_slist_free (undo->redo_levels);

	g_free (undo);
}


static void
action_do_and_destroy_redo (HTMLEngine *engine,
                            HTMLUndo *undo,
                            GList **stack,
                            HTMLUndoDirection dir)
{
	HTMLUndoAction *action;
	GList *first;

	first  = *stack;
	action = HTML_UNDO_ACTION (first->data);

	html_cursor_jump_to_position (engine->cursor, engine, action->position);
	(* action->function) (engine, action->data, dir, action->position_after);
	html_cursor_jump_to_position (engine->cursor, engine, action->position_after);

	*stack = g_list_remove (first, first->data);
	if (undo->level == 0) {
		html_undo_action_destroy (action);

		first = undo->undo_used.stack;
		if (first) {
			html_undo_action_destroy (HTML_UNDO_ACTION (first->data));
			undo->undo_used.stack = g_list_remove (first, first->data);
		}
	}
}

static void
action_do_and_destroy_undo (HTMLEngine *engine,
                            HTMLUndo *undo,
                            HTMLUndoDirection dir)
{
	HTMLUndoAction *action;
	GList *first;

	first  = undo->undo.stack;
	action = HTML_UNDO_ACTION (first->data);

	html_cursor_jump_to_position (engine->cursor, engine, action->position);
	(* action->function) (engine, action->data, dir, action->position_after);
	html_cursor_jump_to_position (engine->cursor, engine, action->position_after);

	undo->undo.stack = g_list_remove (first, first->data);
	if (undo->level == 0) {
		undo->undo_used.stack = g_list_prepend (undo->undo_used.stack, action);
		undo->step_counter--;

		html_engine_emit_undo_changed (engine);
	}
}

void
html_undo_do_undo (HTMLUndo *undo,
                   HTMLEngine *engine)
{
	g_return_if_fail (undo != NULL);
	g_return_if_fail (engine != NULL);

	if (undo->freeze_count > 0)
		return;

	if (undo->undo.size > 0) {
#ifdef UNDO_DEBUG
		if (!undo->level) {
			printf ("UNDO begin\n");
			DEBUG (undo);
		}
#endif
		engine->block_events++;
		action_do_and_destroy_undo (engine, undo, HTML_UNDO_UNDO);
		undo->undo.size--;
		engine->block_events--;
#ifdef UNDO_DEBUG
		if (!undo->level) {
			DEBUG (undo);
			printf ("UNDO end\n");
		}
#endif
	}
}

void
html_undo_do_redo (HTMLUndo *undo,
                   HTMLEngine *engine)
{
	g_return_if_fail (undo != NULL);
	g_return_if_fail (engine != NULL);

	if (undo->freeze_count > 0)
		return;

	if (undo->redo.size > 0) {
#ifdef UNDO_DEBUG
		if (!undo->level) {
			printf ("REDO begin\n");
			DEBUG (undo);
		}
#endif
		undo->in_redo++;
		engine->block_events++;
		action_do_and_destroy_redo (engine, undo, &undo->redo.stack, HTML_UNDO_REDO);
		undo->redo.size--;
		engine->block_events--;
		undo->in_redo--;

#ifdef UNDO_DEBUG
		if (!undo->level) {
			DEBUG (undo);
			printf ("REDO end\n");
		}
#endif
	}
}


void
html_undo_discard_redo (HTMLUndo *undo)
{
	g_return_if_fail (undo != NULL);

	if (undo->freeze_count > 0)
		return;

	if (undo->redo.stack == NULL)
		return;

	destroy_action_list (undo->redo.stack);

	undo->redo.stack = NULL;
	undo->redo.size = 0;
}

void
html_undo_add_undo_action (HTMLUndo *undo,
                            HTMLEngine *engine,
                            HTMLUndoAction *action)
{
	g_return_if_fail (undo != NULL);
	g_return_if_fail (action != NULL);

	if (undo->freeze_count > 0)
		return;

	if (undo->level == 0) {
		if (undo->in_redo == 0 && undo->redo.size > 0)
			add_used_and_redo_to_undo (undo, engine);

		if (undo->undo.size >= HTML_UNDO_LIMIT) {
			HTMLUndoAction *last_action;
			GList *last;

			last = g_list_last (undo->undo.stack);
			last_action = (HTMLUndoAction *) last->data;

			undo->undo.stack = g_list_remove_link (undo->undo.stack, last);
			g_list_free (last);

			html_undo_action_destroy (last_action);

			undo->undo.size--;
		}

		undo->step_counter++;

		html_engine_emit_undo_changed (engine);
	}

	undo->undo.stack = g_list_prepend (undo->undo.stack, action);
	undo->undo.size++;

#ifdef UNDO_DEBUG
	if (!undo->level) {
		printf ("ADD UNDO\n");
		DEBUG (undo);
	}
#endif
}

void
html_undo_add_redo_action (HTMLUndo *undo,
                            HTMLUndoAction *action)
{
	g_return_if_fail (undo != NULL);
	g_return_if_fail (action != NULL);

	if (undo->freeze_count > 0)
		return;

	undo->redo.stack = g_list_prepend (undo->redo.stack, action);
	undo->redo.size++;
}

void
html_undo_add_action (HTMLUndo *undo,
                       HTMLEngine *engine,
                       HTMLUndoAction *action,
                       HTMLUndoDirection dir)
{
	if (undo->freeze_count > 0)
		return;

	if (dir == HTML_UNDO_UNDO)
		html_undo_add_undo_action (undo, engine, action);
	else
		html_undo_add_redo_action (undo, action);
}

void
html_undo_freeze (HTMLUndo *undo)
{
	undo->freeze_count++;
}

void
html_undo_thaw (HTMLUndo *undo)
{
	undo->freeze_count--;
}

/*
 * undo levels
 *
 * IDEA: it closes number of undo steps into one
 * examples: paste
 *             - it first cuts active selection and then inserts objects
 *               from cut_buffer on actual cursor position
 *             - if you don't use udo levels, it will generate two undo
 *               steps/actions replace
 *             - replace uses paste operation, so when it replaces N occurrences,
 *               it generates 2*N steps (without using undo levels in paste and
 *               replace)
 *
 * usage is simple - just call html_undo_level_begin before using functions
 * with undo and html_undo_level_end after them
 */

#define HTML_UNDO_LEVEL(x) ((HTMLUndoLevel *) x)
struct _HTMLUndoLevel {
	HTMLUndoData data;

	HTMLUndo      *parent_undo;
	HTMLUndoStack  stack;

	gchar    *description[HTML_UNDO_END];
};
typedef struct _HTMLUndoLevel HTMLUndoLevel;

static void undo_step_action (HTMLEngine *e, HTMLUndoData *data, HTMLUndoDirection dir, guint position_after);
static void redo_level_begin (HTMLUndo *undo, const gchar *redo_desription, const gchar *undo_desription);
static void redo_level_end   (HTMLUndo *undo);

static void
level_destroy (HTMLUndoData *data)
{
	HTMLUndoLevel *level;

	g_assert (data);

	level = HTML_UNDO_LEVEL (data);

	destroy_action_list (level->stack.stack);
	g_list_free (level->stack.stack);

	g_free (level->description[HTML_UNDO_UNDO]);
	g_free (level->description[HTML_UNDO_REDO]);
}

static HTMLUndoLevel *
level_new (HTMLUndo *undo,
           HTMLUndoStack *stack,
           const gchar *undo_description,
           const gchar *redo_description)
{
	HTMLUndoLevel *nl = g_new (HTMLUndoLevel, 1);

	html_undo_data_init (HTML_UNDO_DATA (nl));

	stack_copy (stack, &nl->stack);

	nl->data.destroy                 = level_destroy;
	nl->parent_undo                  = undo;
	nl->description[HTML_UNDO_UNDO] = g_strdup (undo_description);
	nl->description[HTML_UNDO_REDO] = g_strdup (redo_description);

	return nl;
}

void
html_undo_level_begin (HTMLUndo *undo,
                       const gchar *undo_desription,
                       const gchar *redo_desription)
{
	undo->undo_levels = g_slist_prepend (undo->undo_levels, level_new (undo, &undo->undo,
									   undo_desription, redo_desription));
	undo->undo.stack  = NULL;
	undo->undo.size   = 0;

	undo->level++;
}

static void
redo_level_begin (HTMLUndo *undo,
                  const gchar *redo_desription,
                  const gchar *undo_desription)
{
	undo->redo_levels = g_slist_prepend (undo->redo_levels, level_new (undo, &undo->redo,
									   undo_desription, redo_desription));
	undo->redo.stack  = NULL;
	undo->redo.size   = 0;

	undo->level++;
}

static void
redo_level_end (HTMLUndo *undo)
{
	HTMLUndoLevel *level;
	HTMLUndoStack  save_redo;
	GSList *head;

	g_assert (undo->redo_levels);

	undo->level--;

	/* preserve current redo stack */
	stack_copy (&undo->redo, &save_redo);

	/* restore last level from levels stack */
	level = HTML_UNDO_LEVEL (undo->redo_levels->data);
	stack_copy (&level->stack, &undo->redo);

	/* fill level with current redo step */
	stack_copy (&save_redo, &level->stack);

	/* add redo step redo action */
	if (save_redo.size) {
		HTMLUndoAction *action;

		/* we use position from last redo action on the stack */
		action = (HTMLUndoAction *) save_redo.stack->data;
		action = html_undo_action_new (
			level->description[HTML_UNDO_REDO],
			undo_step_action,
			HTML_UNDO_DATA (level),
			action->position,
			action->position_after);
		html_undo_add_redo_action (undo, action);
#ifdef UNDO_DEBUG
		action->is_level = TRUE;
#endif
	} else
		html_undo_data_unref (HTML_UNDO_DATA (level));

	head = undo->redo_levels;
	undo->redo_levels = g_slist_remove_link (undo->redo_levels, head);
	g_slist_free (head);
}

void
html_undo_level_end (HTMLUndo *undo,
                     HTMLEngine *engine)
{
	HTMLUndoLevel *level;
	HTMLUndoStack  save_undo;
	GSList *head;

	g_assert (undo->undo_levels);
	g_assert (undo->level);

	undo->level--;

	/* preserve current redo stack */
	stack_copy (&undo->undo, &save_undo);

	/* restore last level from levels stack */
	level = HTML_UNDO_LEVEL (undo->undo_levels->data);
	stack_copy (&level->stack, &undo->undo);

	/* fill level with current undo step */
	stack_copy (&save_undo, &level->stack);

	/* add undo step undo action */
	if (save_undo.size) {
		HTMLUndoAction *action;

		/* we use position from last undo action on the stack */
		action = html_undo_action_new (level->description[HTML_UNDO_UNDO],
					       undo_step_action,
					       HTML_UNDO_DATA (level),
					       HTML_UNDO_ACTION (save_undo.stack->data)->position,
					       HTML_UNDO_ACTION (save_undo.stack->data)->position_after);
#ifdef UNDO_DEBUG
		action->is_level = TRUE;
#endif
		html_undo_add_undo_action (undo, engine, action);
	} else
		html_undo_data_unref (HTML_UNDO_DATA (level));

	head = undo->undo_levels;
	undo->undo_levels = g_slist_remove_link (undo->undo_levels, head);
	g_slist_free (head);
}

static void
undo_step_action (HTMLEngine *e,
                  HTMLUndoData *data,
                  HTMLUndoDirection dir,
                  guint position_after)
{
	HTMLUndo      *undo;
	HTMLUndoLevel *level;
	HTMLUndoStack  save;
	HTMLUndoStack *stack;

	level = HTML_UNDO_LEVEL (data);
	undo  = level->parent_undo;
	stack = dir == HTML_UNDO_UNDO ? &undo->undo : &undo->redo;

	/* prepare undo/redo step */
	if (dir == HTML_UNDO_UNDO)
		redo_level_begin (undo, level->description[HTML_UNDO_REDO], level->description[HTML_UNDO_UNDO]);
	else
		html_undo_level_begin (undo, level->description[HTML_UNDO_UNDO], level->description[HTML_UNDO_REDO]);

	/* preserve current undo/redo stack */
	stack_copy (stack, &save);

	/* set this level */
	stack_dup (&level->stack, stack);

	undo->level++;
	if (dir == HTML_UNDO_UNDO)
		while (undo->undo.size)
			html_undo_do_undo (undo, e);
	else
		while (undo->redo.size)
			html_undo_do_redo (undo, e);
	undo->level--;

	/* restore current undo/redo stack */
	stack_copy (&save, stack);

	/* end redo/undo step */
	if (dir == HTML_UNDO_UNDO)
		redo_level_end (undo);
	else
		html_undo_level_end (undo, e);
}

void
html_undo_data_init (HTMLUndoData *data)
{
	data->ref_count = 1;
	data->destroy   = NULL;
}

void
html_undo_data_ref (HTMLUndoData *data)
{
	g_assert (data);

	data->ref_count++;
}

void
html_undo_data_unref (HTMLUndoData *data)
{
	g_assert (data);
	g_assert (data->ref_count > 0);

	data->ref_count--;

	if (data->ref_count == 0) {
		if (data->destroy)
			(*data->destroy) (data);
		g_free (data);
	}
}

HTMLUndoDirection
html_undo_direction_reverse (HTMLUndoDirection dir)
{
	return dir == HTML_UNDO_UNDO ? HTML_UNDO_REDO : HTML_UNDO_UNDO;
}

static void
add_used_and_redo_to_undo (HTMLUndo *undo,
                           HTMLEngine *engine)
{
	GList *stack;
	GList *cur;

	stack            = g_list_reverse (undo->redo.stack);
	undo->redo.stack = NULL;
	undo->redo.size  = 0;

	/* add undo_used */
	for (cur = undo->undo_used.stack; cur; cur = cur->next)
		html_undo_add_undo_action (undo, engine, HTML_UNDO_ACTION (cur->data));
	g_list_free (undo->undo_used.stack);
	undo->undo_used.stack = NULL;

	for (cur = stack; cur; cur = cur->next)
		html_undo_add_undo_action (undo, engine, HTML_UNDO_ACTION (cur->data));
	g_list_free (stack);

#ifdef UNDO_DEBUG
	printf ("REVERSED\n");
	DEBUG (undo);
#endif
}

#ifdef UNDO_DEBUG

static void
print_stack (GList *stack,
             guint size,
             gint l)
{
	gint i;

	for (; stack; stack = stack->next) {
		HTMLUndoAction *action;
		for (i = 0; i < l; i++) printf ("  ");
		action = HTML_UNDO_ACTION (stack->data);
		printf ("%s\n", action->description);
		if (action->is_level) {
			HTMLUndoLevel *level;

			level = (HTMLUndoLevel *) action->data;

			print_stack (level->stack.stack, level->stack.size, l + 1);
		}
	}
}

static void
html_undo_debug (HTMLUndo *undo)
{
	printf ("Undo stack (%d):\n", undo->undo.size);
	print_stack (undo->undo.stack, undo->undo.size, 1);
	printf ("Redo stack (%d):\n", undo->redo.size);
	print_stack (undo->redo.stack, undo->redo.size, 1);
	printf ("Used stack (%d):\n", undo->undo_used.size);
	print_stack (undo->undo_used.stack, undo->undo_used.size, 1);
	printf ("--\n");
}

#endif

void
html_undo_reset (HTMLUndo *undo)
{
	g_return_if_fail (undo != NULL);
	g_return_if_fail (undo->level == 0);

	destroy_action_list (undo->undo.stack);
	destroy_action_list (undo->undo_used.stack);
	destroy_action_list (undo->redo.stack);

	undo->undo.stack = NULL;
	undo->undo.size  = 0;
	undo->undo_used.stack = NULL;
	undo->undo_used.size  = 0;
	undo->redo.stack = NULL;
	undo->redo.size  = 0;

	undo->step_counter = 0;
}

gboolean
html_undo_has_undo_steps (HTMLUndo *undo)
{
	g_assert (undo->step_counter >= 0);

	return undo->step_counter > 0;
}

gint
html_undo_get_step_count (HTMLUndo *undo)
{
	return undo->step_counter;
}
