/* 
 * tkUndo.c --
 *
 *	This module provides the implementation of an undo stack.
 *
 * Copyright (c) 2002 by Ludwig Callewaert.
 *
 * See the file "license.terms" for information on usage and redistribution
 * of this file, and for a DISCLAIMER OF ALL WARRANTIES.
 *
 * RCS: @(#) $Id: tkUndo.c,v 1.6 2004/10/05 01:26:10 hobbs Exp $
 */

#include "tkUndo.h"

static int EvaluateActionList _ANSI_ARGS_ ((Tcl_Interp *interp, 
					    TkUndoSubAtom *action));


/*
 *----------------------------------------------------------------------
 *
 * TkUndoPushStack --
 *	Push elem on the stack identified by stack.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

void
TkUndoPushStack(stack, elem)
    TkUndoAtom **stack, *elem;
{ 
    elem->next = *stack;
    *stack = elem;
}

/*
 *----------------------------------------------------------------------
 *
 * TkUndoPopStack --
 *	Remove and return the top element from the stack identified by 
 *	stack.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

TkUndoAtom *
TkUndoPopStack(stack)
    TkUndoAtom **stack;
{ 
    TkUndoAtom *elem = NULL;
    if (*stack != NULL) {
	elem = *stack;
	*stack = elem->next;
    }
    return elem;
}

/*
 *----------------------------------------------------------------------
 *
 * TkUndoInsertSeparator --
 *	Insert a separator on the stack, indicating a border for
 *	an undo/redo chunk.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

int
TkUndoInsertSeparator(stack)
    TkUndoAtom **stack;
{
    TkUndoAtom *separator;

    if (*stack!=NULL && (*stack)->type!=TK_UNDO_SEPARATOR) {
	separator = (TkUndoAtom *) ckalloc(sizeof(TkUndoAtom));
	separator->type = TK_UNDO_SEPARATOR;
	TkUndoPushStack(stack,separator);
	return 1;
    }
    return 0;
}

/*
 *----------------------------------------------------------------------
 *
 * TkUndoClearStack --
 *	Clear an entire undo or redo stack and destroy all elements in it.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

void
TkUndoClearStack(stack)
    TkUndoAtom **stack;		/* An Undo or Redo stack */
{
    TkUndoAtom *elem;

    while ((elem = TkUndoPopStack(stack)) != NULL) {
	if (elem->type != TK_UNDO_SEPARATOR) {
	    TkUndoSubAtom *sub;
	    
	    sub = elem->apply;
	    while (sub->next != NULL) {
		TkUndoSubAtom *next = sub->next;
		
		if (sub->action != NULL) {
		    Tcl_DecrRefCount(sub->action);
		}
		ckfree((char *)sub);
		sub = next;
	    }
	    sub = elem->revert;
	    while (sub->next != NULL) {
		TkUndoSubAtom *next = sub->next;

		if (sub->action != NULL) {
		    Tcl_DecrRefCount(sub->action);
		}
		ckfree((char *)sub);
		sub = next;
	    }
	    sub = elem->revert;
	}
	ckfree((char *)elem);
    }
    *stack = NULL;
}

/*
 *----------------------------------------------------------------------
 *
 * TkUndoPushAction
 *	Push a new elem on the stack identified by stack.
 *	action and revert are given through Tcl_Obj's to which
 *	we will retain a reference.  (So they can be passed in
 *	with a zero refCount if desired).
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

void
TkUndoPushAction(stack, apply, revert)
    TkUndoRedoStack *stack;	/* An Undo or Redo stack */
    TkUndoSubAtom *apply;
    TkUndoSubAtom *revert;
{ 
    TkUndoAtom *atom;

    atom = (TkUndoAtom *) ckalloc(sizeof(TkUndoAtom));
    atom->type = TK_UNDO_ACTION;
    atom->apply = apply;
    atom->revert = revert;

    TkUndoPushStack(&stack->undoStack, atom);
    TkUndoClearStack(&stack->redoStack);
}

/*
 *----------------------------------------------------------------------
 *
 * TkUndoMakeCmdSubAtom
 * 
 *	Create a new undo/redo step which must later be place into an
 *	undo stack with TkUndoPushAction.  This sub-atom, if evaluated,
 *	will take the given command (if non-NULL), find its full Tcl
 *	command string, and then evaluate that command with the list
 *	elements of 'actionScript' appended.
 *	
 *	If 'subAtomList' is non-NULL, the newly created sub-atom
 *	is added onto the end of the linked list of which
 *	'subAtomList' is a part.  This makes it easy to build up
 *	a sequence of actions which will be pushed in one step.
 *	
 *	A refCount is retained on 'actionScript'.
 *	
 *	Note: if the undo stack can persist for longer than the
 *	Tcl_Command provided, the stack will cause crashes when
 *	actions are evaluated.  In this case the 'command' argument
 *	should not be used.  This is the case with peer text widgets,
 *	for example.
 *
 * Results:
 *	The newly created subAtom is returned. It must be passed
 *	to TkUndoPushAction otherwise a memory leak will result.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

TkUndoSubAtom *
TkUndoMakeCmdSubAtom(command, actionScript, subAtomList)
    Tcl_Command command;        /* Tcl command token for actions, may
                                 * be NULL if not needed. */
    Tcl_Obj *actionScript;	/* The script to append to the command
                          	 * to perform the action (may be
                          	 * NULL if the command is not-null). */
    TkUndoSubAtom *subAtomList; /* Add to the end of this list of
                                 * actions if non-NULL */
{ 
    TkUndoSubAtom *atom;

    if (command == NULL && actionScript == NULL) {
	Tcl_Panic("NULL command and actionScript in TkUndoMakeCmdSubAtom");
    }
    
    atom = (TkUndoSubAtom *) ckalloc(sizeof(TkUndoSubAtom));
    atom->command = command;
    atom->funcPtr = NULL;
    atom->clientData = NULL;
    atom->next = NULL;
    atom->action = actionScript;
    if (atom->action != NULL) {
        Tcl_IncrRefCount(atom->action);
    }

    if (subAtomList != NULL) {
	while (subAtomList->next != NULL) {
	    subAtomList = subAtomList->next;
	}
	subAtomList->next = atom;
    }
    return atom;
}

/*
 *----------------------------------------------------------------------
 *
 * TkUndoMakeSubAtom
 * 
 *	Create a new undo/redo step which must later be place into an
 *	undo stack with TkUndoPushAction.  This sub-atom, if evaluated,
 *	will take the given C-funcPtr (which must be non-NULL), and
 *	call it with three arguments: the undo stack's 'interp', 
 *	the 'clientData' given and the 'actionScript'.  The callback
 *	should return a standard Tcl return code (TCL_OK on success).
 *	
 *	If 'subAtomList' is non-NULL, the newly created sub-atom
 *	is added onto the end of the linked list of which
 *	'subAtomList' is a part.  This makes it easy to build up
 *	a sequence of actions which will be pushed in one step.
 *	
 *	A refCount is retained on 'actionScript'.
 *
 * Results:
 *	The newly created subAtom is returned.  It must be passed
 *	to TkUndoPushAction otherwise a memory leak will result.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

TkUndoSubAtom *
TkUndoMakeSubAtom(funcPtr, clientData, actionScript, subAtomList)
    TkUndoProc *funcPtr;        /* Callback function to perform
                                 * the undo/redo. */
    ClientData clientData;      /* Data to pass to the callback
                                 * function. */
    Tcl_Obj *actionScript;	/* Additional Tcl data to pass to
                          	 * the callback function (may be
                          	 * NULL). */
    TkUndoSubAtom *subAtomList; /* Add to the end of this list of
                                 * actions if non-NULL */
{ 
    TkUndoSubAtom *atom;

    if (funcPtr == NULL) {
	Tcl_Panic("NULL funcPtr in TkUndoMakeSubAtom");
    }

    atom = (TkUndoSubAtom *) ckalloc(sizeof(TkUndoSubAtom));
    atom->command = NULL;
    atom->funcPtr = funcPtr;
    atom->clientData = clientData;
    atom->next = NULL;
    atom->action = actionScript;
    if (atom->action != NULL) {
        Tcl_IncrRefCount(atom->action);
    }
    
    if (subAtomList != NULL) {
	while (subAtomList->next != NULL) {
	    subAtomList = subAtomList->next;
	}
	subAtomList->next = atom;
    }
    return atom;
}

/*
 *----------------------------------------------------------------------
 *
 * TkUndoInitStack
 *	Initialize a new undo/redo stack.
 *
 * Results:
 *	An Undo/Redo stack pointer.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

TkUndoRedoStack *
TkUndoInitStack(interp, maxdepth)
    Tcl_Interp *interp;		/* The interpreter */
    int maxdepth;		/* The maximum stack depth */
{ 
    TkUndoRedoStack *stack;	/* An Undo/Redo stack */

    stack = (TkUndoRedoStack *) ckalloc(sizeof(TkUndoRedoStack));
    stack->undoStack = NULL;
    stack->redoStack = NULL;
    stack->interp    = interp;
    stack->maxdepth  = maxdepth; 
    stack->depth     = 0;
    return stack;
}

/*
 *----------------------------------------------------------------------
 *
 * TkUndoInitStack
 *	Initialize a new undo/redo stack.
 *
 * Results:
 *	An Undo/Redo stack pointer.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

void
TkUndoSetDepth(stack, maxdepth)
    TkUndoRedoStack *stack;	/* An Undo/Redo stack */
    int maxdepth;		/* The maximum stack depth */
{
    TkUndoAtom *elem, *prevelem;
    int sepNumber = 0;

    stack->maxdepth = maxdepth;

    if (stack->maxdepth>0 && stack->depth>stack->maxdepth) {
	/*
	 * Maximum stack depth exceeded. We have to remove the last
	 * compound elements on the stack
	 */

	elem = stack->undoStack;
	prevelem = NULL;
	while (sepNumber <= stack->maxdepth) {
	    if (elem != NULL && elem->type == TK_UNDO_SEPARATOR) {
		sepNumber++;
	    }
	    prevelem = elem;
	    elem = elem->next;
	}
	prevelem->next = NULL;
	while (elem != NULL) {
	    TkUndoSubAtom *sub = elem->apply;
	    prevelem = elem;

	    while (sub->next != NULL) {
		TkUndoSubAtom *next = sub->next;
		if (sub->action != NULL) {
		    Tcl_DecrRefCount(sub->action);
		}
		ckfree((char *)sub);
		sub = next;
	    }
	    
	    elem = elem->next;
	    ckfree((char *) prevelem);
	}
	stack->depth = stack->maxdepth;
    }
}

/*
 *----------------------------------------------------------------------
 *
 * TkUndoClearStacks
 *	Clear both the undo and redo stack.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

void
TkUndoClearStacks(stack)
    TkUndoRedoStack *stack;	/* An Undo/Redo stack */
{ 
    TkUndoClearStack(&stack->undoStack);
    TkUndoClearStack(&stack->redoStack);
    stack->depth = 0;
}

/*
 *----------------------------------------------------------------------
 *
 * TkUndoFreeStack
 *	Clear both the undo and redo stack and free the memory
 *	allocated to the u/r stack pointer.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

void
TkUndoFreeStack(stack)
    TkUndoRedoStack *stack;	/* An Undo/Redo stack */
{ 
   TkUndoClearStacks(stack);
   ckfree((char *) stack);
}

/*
 *----------------------------------------------------------------------
 *
 * TkUndoInsertUndoSeparator --
 *	Insert a separator on the undo stack, indicating a border for
 *	an undo/redo chunk.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

void
TkUndoInsertUndoSeparator(stack)
    TkUndoRedoStack *stack;
{
    /*
     * TkUndoAtom * elem;
     * TkUndoAtom * prevelem;
     * int sepNumber = 0;
     */

    if (TkUndoInsertSeparator(&stack->undoStack)) {
	stack->depth++;
	TkUndoSetDepth(stack, stack->maxdepth);
#if 0
	if (stack->maxdepth>0 && stack->depth>stack->maxdepth) {
	    elem = stack->undoStack;
	    prevelem = NULL;
	    while (sepNumber < stack->depth) {
		if (elem != NULL && elem->type == TK_UNDO_SEPARATOR) {
		    sepNumber++;
		}
		prevelem = elem;
		elem = elem->next;
	    }
	    prevelem->next = NULL;
	    while (elem) {
		prevelem = elem;
		elem = elem->next;
		ckfree((char *) elem);
	    }
	    stack->depth;
	}
#endif
    }
}

/*
 *----------------------------------------------------------------------
 *
 * TkUndoRevert --
 *	Undo a compound action on the stack.
 *
 * Results:
 *	A Tcl status code
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

int
TkUndoRevert(stack)
    TkUndoRedoStack *stack;
{
    TkUndoAtom *elem;

    /* insert a separator on the undo and the redo stack */

    TkUndoInsertUndoSeparator(stack);
    TkUndoInsertSeparator(&stack->redoStack);

    /* Pop and skip the first separator if there is one*/

    elem = TkUndoPopStack(&stack->undoStack);

    if (elem == NULL) {
	return TCL_ERROR;
    }

    if (elem != NULL && elem->type == TK_UNDO_SEPARATOR) {
	ckfree((char *) elem);
	elem = TkUndoPopStack(&stack->undoStack);
    }

    while (elem != NULL && elem->type != TK_UNDO_SEPARATOR) {
	/* Note that we currently ignore errors thrown here */
	EvaluateActionList(stack->interp, elem->revert);

	TkUndoPushStack(&stack->redoStack,elem);
	elem = TkUndoPopStack(&stack->undoStack);
    }

    /* insert a separator on the redo stack */

    TkUndoInsertSeparator(&stack->redoStack);

    stack->depth--;

    return TCL_OK;
}

/*
 *----------------------------------------------------------------------
 *
 * TkUndoApply --
 *	Redo a compound action on the stack.
 *
 * Results:
 *	A Tcl status code
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

int
TkUndoApply(stack)
    TkUndoRedoStack *stack;
{
    TkUndoAtom *elem;

    /* insert a separator on the undo stack */

    TkUndoInsertSeparator(&stack->undoStack);

    /* Pop and skip the first separator if there is one*/

    elem = TkUndoPopStack(&stack->redoStack);

    if (elem == NULL) {
	return TCL_ERROR;
    }

    if (elem != NULL && elem->type == TK_UNDO_SEPARATOR) {
	ckfree((char *) elem);
	elem = TkUndoPopStack(&stack->redoStack);
    }

    while (elem != NULL && elem->type != TK_UNDO_SEPARATOR) {
	/* Note that we currently ignore errors thrown here */
	EvaluateActionList(stack->interp, elem->apply);

	TkUndoPushStack(&stack->undoStack, elem);
	elem = TkUndoPopStack(&stack->redoStack);
    }

    /* insert a separator on the undo stack */

    TkUndoInsertSeparator(&stack->undoStack);

    stack->depth++;

    return TCL_OK;
}

/*
 *----------------------------------------------------------------------
 *
 * EvaluateActionList --
 * 
 *	Execute a linked list of undo/redo sub-atoms.  If any sub-atom
 *	returns a non TCL_OK value, execution of subsequent sub-atoms
 *	is cancelled and the error returned immediately.
 *
 * Results:
 *	A Tcl status code
 *
 * Side effects:
 *	The undo/redo subAtoms can perform arbitrary actions.
 *
 *----------------------------------------------------------------------
 */

static int
EvaluateActionList(interp, action)
    Tcl_Interp *interp;     /* Interpreter to evaluate the action in. */
    TkUndoSubAtom *action;  /* Head of linked list of action steps
                             * to perform. */
{
    int result = TCL_OK;

    while (action != NULL) {
	if (action->funcPtr != NULL) {
	    result = (*action->funcPtr)(interp, action->clientData,
					action->action);
	} else if (action->command != NULL) {
	    Tcl_Obj *cmdNameObj, *evalObj;

	    cmdNameObj = Tcl_NewObj();
	    evalObj = Tcl_NewObj();
	    Tcl_IncrRefCount(evalObj);
	    Tcl_GetCommandFullName(interp, action->command, cmdNameObj);
	    Tcl_ListObjAppendElement(NULL, evalObj, cmdNameObj);
	    if (action->action != NULL) {
	        Tcl_ListObjAppendList(NULL, evalObj, action->action);
	    }
	    result = Tcl_EvalObjEx(interp, evalObj, TCL_EVAL_GLOBAL);
	    Tcl_DecrRefCount(evalObj);
	} else {
	    result = Tcl_EvalObjEx(interp, action->action, TCL_EVAL_GLOBAL);
	}
	if (result != TCL_OK) {
	    return result;
	}
	action = action->next;
    }
    return result;
}
