/* undo.c
 *
 * set of procedures to realize Undo and Redo edit operations;
 * maximum number of changes that may be undone is set by EditSTACK_SIZE
 *
 * Last revision - 18.01.1993
 *
 *
 * (C) COPYRIGHT A. Stochniol, 1991 - 1993
 * All Rights Reserved
 *
 * Author:
 * Dr. Andrzej Stochniol
 * Department of Mechanical Engineering
 * Imperial College of Science, Technology and Medicine
 * London, SW7 2BX, UK
 *
 *    E-mail:  A.Stochniol@ic.ac.uk
 *
 *
 */

#ifdef AMIGA
#include "amiga.h"
#endif

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

#include <Xm/Text.h>


/* external declarations */
/* !!!! we should really read in the declarations of constants like MENU_UNDO !!! */
#define MENU_UNDO	        2000

extern Widget   edit_text,
		menu_undo_button, menu_redo_button;
extern Widget   changes_status;
extern Display	*display;				/*  Display	*/
extern Boolean  do_undo_redo_action;
extern long     changes_counter;
extern XmStringCharSet charset;




/* declarations specific for undo */

/* size of the undo/redo stacks*/
#define EditSTACK_SIZE		100


/* in Motif XmTextBlock's are used to pass text around and the internal
   structure is as follow
  typedef struct {
    char *ptr;                  |* Pointer to data. *|
    int length;                 |* Number of bytes of data. *|
    XmTextFormat format;        |* Representation format. *|
  } XmTextBlockRec, *XmTextBlock;
*/


typedef struct
{
   int reason;
   long currInsert, newInsert;
   long startPos, endPos;
   char *del_text, *ins_text;
}  EditAction, *EditActionPtr;

typedef struct
{
   int 		bottom;			/* bootom (head) of the stack */
   int 		top;			/* top (tail) of the stack */
   EditActionPtr el[EditSTACK_SIZE];	/* pointers to stack elements */
}  EditStack;


#ifdef _NO_PROTO
void flush_edit_stack(/* EditStack *s */);
void pop_edit_stack (/* EditStack *s, EditActionPtr *el */);
void push_edit_stack(/* EditStack *s, EditActionPtr el */);
#else
void flush_edit_stack(EditStack *s);
void pop_edit_stack (EditStack *s, EditActionPtr *el);
void push_edit_stack(EditStack *s, EditActionPtr el);
#endif


EditStack undo_stack = {0, 0};
EditStack redo_stack = {0, 0};



#ifdef _NO_PROTO
void push_edit_stack(s, el)
    EditStack *s;
    EditActionPtr el;
#else  /* _NO_PROTO */

void push_edit_stack(EditStack *s, EditActionPtr el)
#endif
{
    /* push an element on the edit stack (on its top); the stack has predefined
       maximum length (EditSTACK_SIZE); if the stack is filled up one element
       from the bottom of the stack is destroyed, address of the bottom is moved
       up so we get one space on the stack. In fact it is not a classic
       stack which I implemented here but this "cyclic" version gives an easy
       way of restoring last EditSTACK_SIZE edit commands;
       because of the possibility of moving of the stack bottom the address
       of any element on the stack must be calculated "modulo" EditSTACK_SIZE;
    */

    s->el[s->top] = el;
    s->top = (s->top + 1) % EditSTACK_SIZE;

    /* check if the stack is used up completely; if so destroy the oldest
       element (i.e. from the bottom of the stack) and move the address
       of the bottom
    */
    if(s->bottom == s->top)
    {
	/* if memory was allocated for the text free it */
	if(s->el[s->bottom]->del_text)
	    { XtFree(s->el[s->bottom]->del_text); s->el[s->bottom]->del_text = NULL; }
	if(s->el[s->bottom]->ins_text)
	    { XtFree(s->el[s->bottom]->ins_text); s->el[s->bottom]->ins_text = NULL; }

	s->bottom = (s->bottom + 1) % EditSTACK_SIZE;
    }
} /* push_edit_stack */

#ifdef _NO_PROTO
void pop_edit_stack(s, el)
    EditStack *s;
    EditActionPtr *el;
#else  /* _NO_PROTO */

void pop_edit_stack(EditStack *s, EditActionPtr *el)
#endif
{
    /* pop the element from the top of the edit stack s */
    if(s->bottom != s->top)
    {
	s->top --;
	if(s->top < 0) s->top += EditSTACK_SIZE;	/* "cyclic" stack */
	*el = s->el[s->top];				/* moved here to avoid
							   checking twice for sign
							   of s->topp -- */
    }
    else *el = NULL;					/* empty stack */
} /* pop_edit_stack */


#ifdef _NO_PROTO
void flush_edit_stack(s)
    EditStack *s;
#else  /* _NO_PROTO */

void flush_edit_stack(EditStack *s)
#endif
{
    /* flush the edit stack pointed by s */
    int i, j, n;
    n = s->top - s->bottom;
    if(n < 0) n += EditSTACK_SIZE;		/* "cyclic" stack */

    for(i=0; i<n; i++)
    {
	j = s->top -i -1;
	if(j < 0) j += EditSTACK_SIZE;
	/* if memory was allocated for the text free it */
	if(s->el[j]->del_text)
	    { XtFree(s->el[j]->del_text); s->el[j]->del_text = NULL; }

	if(s->el[j]->ins_text)
	    { XtFree(s->el[j]->ins_text); s->el[j]->ins_text = NULL; }
    }
    s->top = s->bottom;
} /* flush_edit_stack */


#ifdef _NO_PROTO
void reset_undo_redo()
#else  /* _NO_PROTO */

void reset_undo_redo(void)
#endif
{
    /* resets undo/redo by flushing redo and undo stacks */

    flush_edit_stack(&redo_stack);
    flush_edit_stack(&undo_stack);
}   /* reset_undo_redo */


#ifdef _NO_PROTO
void SaveActionForUndo(w, client_data, call_data)
    Widget w;
    caddr_t client_data;
    caddr_t call_data;
#else  /* _NO_PROTO */

void SaveActionForUndo(Widget w, caddr_t client_data, caddr_t call_data)
#endif
{
    /* save the current XmTextVerifyCallbackStruct for later use; additionally
       for delete operation save the deleted text in the undo_text->text->ptr;
       for an easy access in undo/redo I terminate undo_text->text->ptr with a
       NULL (so it can be used straight away in the, for example, XmTextReplace).
    */

    XmTextVerifyPtr txt_data = (XmTextVerifyPtr) call_data;
    EditActionPtr previous_undo;
    String text_buffer;
    String text_changed;
    Arg    al[2];
    int    reason;		/* reason for the callback */
    int    len;

    EditActionPtr  undo_el;	/* txt_data for undo operation */

    /* to speed the process of editing we preallocate a buffer for edit_history,
       with exactly the same length as EditSTACK_SIZE; this get rid of permanent
       allocation and deallocation of memory for every edit action; the memory
       must be only allocated/deallocated to store text added (not for all
       variables describing the edit operation) */
    static EditAction edit_history[EditSTACK_SIZE];
    static Boolean edit_history_initialized = False;
    int i;

    /* when you start editing allocate memory for text blocks in edit_history */
    if(!edit_history_initialized)
    {
	edit_history_initialized = True;
	for(i=0; i < EditSTACK_SIZE; i++) 
	{
	
	edit_history[i].del_text = NULL;
	edit_history[i].ins_text = NULL;

	; /* was needed in the version with text blocks */
	}
	/****OLD   edit_history[i].text =
			 (XmTextBlock) XtMalloc(sizeof(XmTextBlockRec));
	*****/
    }

    /* first check if the previous undo_txt was connected with the mouse operation;
       if so and the current operation is move do not save the current move
       operation;  WE ONLY save first new move operation !!!
    */
    pop_edit_stack(&undo_stack, &previous_undo);
    if(previous_undo == NULL && txt_data->reason == XmCR_MOVING_INSERT_CURSOR )  return;   /* do not store initial movements */
    if(previous_undo != NULL )			 /* the stack is not empty */
    {
	push_edit_stack(&undo_stack, previous_undo);	/* push it back ... */
	if(previous_undo->reason == XmCR_MOVING_INSERT_CURSOR &&
	   txt_data->reason      == XmCR_MOVING_INSERT_CURSOR )
	{
	    return;			/* RETURN - do not store successive moves */
	}
    }


    /* the undo_el is a pointer to an element in edit_history, the
       element in edit_history has the same index as the top element on the
       undo_stack (which has a "cyclic" nature)
    */
    undo_el = &edit_history[undo_stack.top];


    /* if there was anything on the redo stack flush it (we are just about to
       put a new entry on the undo stack); make the redo button insensitive  */
    flush_edit_stack(&redo_stack);
    XtSetSensitive(menu_redo_button,  False);

    reason = txt_data->reason;

    switch(reason)
    {
	case XmCR_MODIFYING_TEXT_VALUE:
	    /* for a modifying of the edit text allocate memory and the text;
	       there may be two cases: text addition or deletion;  */
	    len = txt_data->text->length;
	    if(len > 0)
	    {	/* text adding */
		undo_el->ins_text = (String) XtMalloc((len+1) * sizeof(char)); /* + NULL */
		strncpy(undo_el->ins_text, txt_data->text->ptr, len); /* can't use strcpy
						because ptr field is not NULL terminated */
		/* terminate the string with NULL (for easy use in undo/redo) */
		undo_el->ins_text[len] = NULL;
	    }

	    /* now check if any data was deleted or not (if so store it) */
	    len = (int) (txt_data->endPos - txt_data->startPos); /* amount of text deleted */
	    if(len > 0)
	    {	/* text deleting ...(or just replacing with pending delete)
		 - store it but do not change text->length.
		   it will be used to recognise both those cases */
		undo_el->del_text = (String) XtMalloc((len+1) * sizeof(char));

		/* get the address of the internally stored text */
		XtSetArg(al[0], XmNvalue, &text_buffer);
		XtGetValues(w, al, 1);
		strncpy(undo_el->del_text, text_buffer+txt_data->startPos,len);
#ifndef GET_TEXT_VALUE_BUG_FIXED
		/* see the explanation of GET_TEXT_VALUE_BUG_FIXED in the asedit.c */
		XtFree(text_buffer);
#endif	/* GET_TEXT_VALUE_BUG_FIXED */
		undo_el->del_text[len] = NULL;

	    }
	    break;

	case XmCR_MOVING_INSERT_CURSOR:
	    break;	/* no text to store ... */
    }
    /* set the rest of undo_el .... */
    undo_el->reason 	= txt_data->reason;
    undo_el->currInsert 	= txt_data->currInsert;
    undo_el->newInsert 	= txt_data->newInsert;
    undo_el->startPos 	= txt_data->startPos;
    undo_el->endPos	= txt_data->endPos;

    /* push the undo data on the undo stack ... */
    push_edit_stack(&undo_stack, undo_el);
    /* set sensitivity to the undo button */
    XtSetSensitive(menu_undo_button, True);

} /* SaveActionForUndo */


/*****************************  UndoRedo  *************************************
**
**	Process undo or redo actions for edit_text widget.
**      The kind of the action is set via action_type.
**      !!!!! The structure of the procedure is nearly the same as TextCB
**      so if you make changes there or here remeber to make the changes in
**      the other procedure. For full comments see TextCB !!!
*/
#ifdef _NO_PROTO
void UndoRedo (action_type)
    int action_type;
#else  /* _NO_PROTO */

void UndoRedo (int action_type)
#endif
{

    EditActionPtr    txt_data=NULL;
    int    reason;		  /* reason for the callback in TextCB */
    XmTextPosition   insert_len;  /* length of text to be inserted */
    XmTextPosition   start, end;

    long ins_len, del_len;


    if(action_type == MENU_UNDO)
    {
	pop_edit_stack(&undo_stack, &txt_data);
	if(undo_stack.bottom == undo_stack.top)  /* last element was popped... */
	{
	    XtSetSensitive(menu_undo_button,  False);
	    XFlush(display);	/* sensitivity must be set before next keys are
				   processed, otherwise with a busy system we
				   could "skip over" the bottom of the stack
				   and all track would be lost !!! */
	}
    }
    else
    {
	 pop_edit_stack(&redo_stack, &txt_data);
	 if(redo_stack.bottom == redo_stack.top)  /* last element was popped... */
	 {
	     XtSetSensitive(menu_redo_button,  False);
	     XFlush(display);	/* as above; be sure that the item is insensitive */
	 }

    }

    if(txt_data == NULL) 	return;	/* should not happen, we are doing this just in
				   case. If it did happen it would mean that
				   the undo/redo was called when the appropriate
				   stack was empty, i.e. the sensitivity of the
				   appropriate button was set improperly (but now
				   would set correctly) */


    /* push the obtained element onto the OPPOSITE stack & set sensitivity */
    if(action_type == MENU_UNDO)
    {
	 push_edit_stack(&redo_stack, txt_data);
	 XtSetSensitive(menu_redo_button,  True);
    }
    else
    {
	 push_edit_stack(&undo_stack, txt_data);
	 XtSetSensitive(menu_undo_button,  True);
    }



    /* set the logical value do_undo_redo_action to True */
    do_undo_redo_action = True;

    /*  finally do the appropriate undo/redo operation .... */
    reason = txt_data->reason;

    switch(reason)
    {
	case XmCR_MODIFYING_TEXT_VALUE:
	    /* increase (redo) or decrease (undo) the changes counter ... */
	    if(action_type == MENU_UNDO)
	    {
		changes_counter --;
		if(changes_counter == 0L ) write_lw(changes_status, charset, " ");
		if(changes_counter == -1L ) write_lw(changes_status, charset, "*");

	    }
	    else
	    {
		changes_counter ++;	/* redo */
		if(changes_counter == 1L ) write_lw(changes_status, charset, "*");
	    }


	    /* for the undo operation the action must be reversed !!!
	       (what was stored	on the undo stack is a plain copy of the
	       operation performed, with only one addition for the delete
	       operation, when the text was stored but the
	       length field was not changed)
	    */
	    /* for SG there is a problem with a strlen function, if the
		argument is NULL we get the coredump !!! */
	    if(txt_data->ins_text) ins_len = strlen(txt_data->ins_text);
	    else 		   ins_len = 0;
	    if(txt_data->del_text) del_len = strlen(txt_data->del_text);  
	    else		   del_len = 0;

	    if(action_type == MENU_UNDO)
	    {
		start      = txt_data->startPos;
		end        = txt_data->startPos + ins_len;
		XmTextReplace(edit_text, start, end, txt_data->del_text);
	    }
	    else	/* redo */
	    {
		start      = txt_data->startPos;
		end        = txt_data->endPos;
		XmTextReplace(edit_text, start, end, txt_data->ins_text);
	    }

	    /* for highlighting we can use:
	       XmTextSetHighlight(edit_text, left, right, XmHIGHLIGHT_SELECTED);
							  XmHIGHLIGHT_NORMAL  - not highlighted
							  XmHIGHLIGHT_SECONDARY_SELECTED - underlined
	    ****/

	    /******* TEMP DEBUG
		fprintf(stderr, "changes_counter = %ld\n", changes_counter); ****/
	    break;

	case XmCR_MOVING_INSERT_CURSOR:
	    if(action_type == MENU_UNDO)
		XmTextSetInsertionPosition(edit_text, txt_data->currInsert);
	    else
		XmTextSetInsertionPosition(edit_text, txt_data->newInsert); /* redo */

	    break;

	default:
	    /* an unknown client_data was received and there is no setup to handle this
	       -  do  nothing; end procedure nicely  */
	    fprintf(stderr, "Warning: an unknown client_data in undo_redo callback\n");
	    fprintf(stderr,"Detected reason = %d \n", reason);
	    break;
    }

} /* UndoRedo */

