/* undo.c -- Recording and use of undo information
   Copyright (C) 1994 John Harper <jsh@ukc.ac.uk>

   This file is part of Jade.

   Jade 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, or (at your option)
   any later version.

   Jade 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 Jade; see the file COPYING. If not, write to
   the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.  */

#include "jade.h"
#include "jade_protos.h"

#include <string.h>

_PR void  undo_record_deletion(TX *, POS *, POS *);
_PR VALUE undo_push_deletion(TX *, POS *, POS *);
_PR void  undo_record_insertion(TX *, POS *, POS *);
_PR void  undo_record_modification(TX *, POS *, POS *);
_PR void  undo_distinct(void);
_PR void  undo_new_group(void);
_PR void  undo_trim(void);
_PR void  undo_init(void);

/* Maximum number of bytes that *each buffer* may devote to undo
   information.  */
static max_undo_size = 10000;

/* Lets us use the string which undo_record_deletion() creates for
   other uses.	*/
static VALUE pending_deletion_string;
static POS pending_deletion_start, pending_deletion_end;
static TX *pending_deletion_tx;

/* A new command has been invoked but the group-separator hasn't been
   added yet. */
static bool pending_group;

/* While we're in cmd_undo() this is set.  This is also tested by
   undo_distinct(); if FALSE it will call coalesce_undo() if necessary.
   undo_distinct() always sets this to FALSE. */
static bool in_undo;
static TX *last_undid_tx;

static VALUE sym_undo;

/* If not in an undo, this will re-combine the waiting_undo and
   tx_UndoList. */
static void
coalesce_undo(TX *tx)
{
    if(!in_undo
       && (tx->tx_ToUndoList != NULL))
    {
	VALUE tmp = cmd_nreverse(tx->tx_UndoneList);
	if(tmp)
	{
	    tx->tx_UndoList = cmd_nconc(list_3(tx->tx_UndoList,
					       tmp,
					       tx->tx_ToUndoList));
	}
	tx->tx_UndoneList = sym_nil;
	tx->tx_ToUndoList = NULL;
	last_undid_tx = NULL;
    }
}

/* Called *after* recording an undo command, checks if this is the
   first change to the buffer. Should be called before the operation. */
static INLINE void
check_first_mod(TX *tx)
{
    if((tx->tx_Changes == tx->tx_ProperSaveChanges)
       && ((tx->tx_Flags & TXFF_NO_UNDO) == 0))
    {
	/* First modification, record this. */
	tx->tx_UndoList = cmd_cons(sym_t, tx->tx_UndoList);
    }
}

/* If pending_group is set a group-separator is added to the buffer's
   undo list. */
static void
check_group(TX *tx)
{
    if(pending_group
#if 0
       && !NILP(tx->tx_UndoList)
#endif
       && ((tx->tx_Flags & TXFF_NO_UNDO) == 0))
    {
	tx->tx_UndoList = cmd_cons(sym_nil, tx->tx_UndoList);
    }
    pending_group = FALSE;
}

/* Grabs the string between START and END in buffer TX and adds it to
   the buffer's undo-list.  This has to be done *before* the text is
   actually deleted from the buffer (for obvious reasons).  */
void
undo_record_deletion(TX *tx, POS *start, POS *end)
{
    if((tx->tx_Flags & TXFF_NO_UNDO) == 0)
    {
	VALUE string;
	VALUE lstart = make_lpos(start);
	if((pending_deletion_string != NULL)
	   && (pending_deletion_tx = tx)
	   && (POS_EQUAL_P(&pending_deletion_start, start))
	   && (POS_EQUAL_P(&pending_deletion_end, end)))
	{
	    /* A saved deletion; use it. */
	    string = pending_deletion_string;
	}
	else
	{
	    long len = section_length(tx, start, end);
	    if(len == 1)
	    {
		/* A deletion of 1 character is recorded as a character. */
		string = cmd_get_char(lstart, VAL(tx));
		if(!string || !NUMBERP(string))
		    return;
	    }
	    else
	    {
		string = make_string(len + 1);
		copy_section(tx, start, end, VSTR(string));
		VSTR(string)[len] = 0;
	    }
	}
	coalesce_undo(tx);
	check_group(tx);
	check_first_mod(tx);
	tx->tx_UndoList = cmd_cons(cmd_cons(lstart, string),
				   tx->tx_UndoList);
    }
    pending_deletion_string = NULL;
}

/* Lets the saved deletion be used for more than the undo list. Call
   this *before* doing anything else. It will be copy the string and
   return it. The next call to undo_record_deletion() will use the
   *same* copy (unless the parameters don't match).  */
VALUE
undo_push_deletion(TX *tx, POS *start, POS *end)
{
    long len = section_length(tx, start, end);
    VALUE string = make_string(len + 1);
    copy_section(tx, start, end, VSTR(string));
    VSTR(string)[len] = 0;
    pending_deletion_string = string;
    pending_deletion_start = *start;
    pending_deletion_end = *end;
    pending_deletion_tx = tx;
    return(string);
}

/* Adds an insertion between START and END to the TX buffer's undo-list.
   Doesn't copy anything, just records START and END.  */
void
undo_record_insertion(TX *tx, POS *start, POS *end)
{
    if((tx->tx_Flags & TXFF_NO_UNDO) == 0)
    {
	VALUE item;
	coalesce_undo(tx);
	check_group(tx);
	check_first_mod(tx);
	item = tx->tx_UndoList;
	if(CONSP(item) && CONSP(VCAR(item)))
	{
	    item = VCAR(item);
	    if(POSP(VCDR(item)) && POS_EQUAL_P(start, &VPOS(VCDR(item))))
	    {
		/* This insertion is directly after the end of the
		   previous insertion; extend the previous one to cover
		   this one.  */
		VPOS(VCDR(item)) = *end;
		return;
	    }
	}
	tx->tx_UndoList = cmd_cons(cmd_cons(make_lpos(start), make_lpos(end)),
				   tx->tx_UndoList);
    }
}

/* Record that the text between START and END has been modified.  This
   must be done *before* the modification is actually done.  */
void
undo_record_modification(TX *tx, POS *start, POS *end)
{
    if((tx->tx_Flags & TXFF_NO_UNDO) == 0)
    {
	undo_record_deletion(tx, start, end);
	undo_record_insertion(tx, start, end);
    }
}

/* Signal the end of this command. */
void
undo_distinct(void)
{
    if((!last_command || !NILP(last_command))
       && (last_command != sym_undo)
       && last_undid_tx
       && last_undid_tx->tx_ToUndoList)
    {
	coalesce_undo(last_undid_tx);
    }
    in_undo = FALSE;
}

/* A new command is started. */
void
undo_new_group(void)
{
    pending_group = TRUE;
}

_PR VALUE cmd_undo(VALUE tx);
DEFUN_INT("undo", cmd_undo, subr_undo, (VALUE tx), V_Subr1, DOC_undo, "") /*
::doc:undo::
undo [BUFFER]

In the buffer BUFFER, undo everything back to the start of the previous
command. Consecutive undo commands work backwards through the BUFFER's
history.
::end:: */
{
    if(!BUFFERP(tx))
	tx = VAL(curr_vw->vw_Tx);
    if(VTX(tx)->tx_ToUndoList == NULL)
    {
	/* First call. */
	VTX(tx)->tx_ToUndoList = VTX(tx)->tx_UndoList;
	VTX(tx)->tx_UndoList = sym_nil;
    }
    if(NILP(VTX(tx)->tx_ToUndoList))
	return(cmd_signal(sym_error, LIST_1(MKSTR("Nothing to undo!"))));
    in_undo = TRUE;
    last_undid_tx = VTX(tx);
    while(CONSP(VTX(tx)->tx_ToUndoList))
    {
	VALUE item = VCAR(VTX(tx)->tx_ToUndoList);
	VTX(tx)->tx_ToUndoList = VCDR(VTX(tx)->tx_ToUndoList);
	VTX(tx)->tx_UndoneList = cmd_cons(item, VTX(tx)->tx_UndoneList);
	if(NILP(item))
	    break;		/* Group separator; break the loop. */
	else if(CONSP(item) && POSP(VCAR(item)))
	{
	    if(STRINGP(VCDR(item)))
	    {
		/* A deleted string */
		VALUE new = cmd_insert(VCDR(item), VCAR(item), tx);
		if(new && POSP(new))
		    cmd_goto_char(new);
	    }
	    else if(NUMBERP(VCDR(item)))
	    {
		/* A deleted character */
		VALUE tmp = make_string(2);
		VSTR(tmp)[0] = (u_char)VNUM(VCDR(item));
		VSTR(tmp)[1] = 0;
		tmp = cmd_insert(tmp, VCAR(item), tx);
		if(tmp && POSP(tmp))
		    cmd_goto_char(tmp);
	    }
	    else if(POSP(VCDR(item)))
	    {
		cmd_delete_area(VCAR(item), VCDR(item), tx); /* insert */
		cmd_goto_char(VCAR(item));
	    }
	}
	else if(POSP(item))
	{
	    cmd_goto_char(item);
	}
	else if(item == sym_t)
	{
	    /* clear modification flag. */
	    cmd_set_buffer_modified(tx, sym_nil);
	}
    }
    this_command = sym_undo;
    return(sym_t);
}

_PR VALUE var_max_undo_size(VALUE val);
DEFUN("max-undo-size", var_max_undo_size, subr_max_undo_size, (VALUE val), V_Var, DOC_max_undo_size) /*
::doc:max_undo_size::
The maximum amount of storage that a single buffer may devote to recording
undo information.
::end:: */
{
    if(val)
    {
	if(NUMBERP(val))
	    max_undo_size = VNUM(val);
	return(NULL);
    }
    else
	return(make_number(max_undo_size));
}

_PR VALUE var_buffer_record_undo(VALUE val);
DEFUN("buffer-record-undo", var_buffer_record_undo, subr_buffer_record_undo, (VALUE val), V_Var, DOC_buffer_record_undo) /*
::doc:buffer_record_undo::
When nil no undo information is kept in this buffer.
::end:: */
{
    TX *tx = curr_vw->vw_Tx;
    if(val)
    {
	if(NILP(val))
	    tx->tx_Flags |= TXFF_NO_UNDO;
	else
	    tx->tx_Flags &= ~TXFF_NO_UNDO;
	return(NULL);
    }
    else
	return((tx->tx_Flags & TXFF_NO_UNDO) ? sym_nil : sym_t);
}

VALUE var_buffer_undo_list(VALUE val);
DEFUN("buffer-undo-list", var_buffer_undo_list, subr_buffer_undo_list, (VALUE val), V_Var, DOC_buffer_undo_list) /*
::doc:buffer_undo_list::
This buffer's list of undo information.
::end:: */
{
    TX *tx = curr_vw->vw_Tx;
    if(val)
	tx->tx_UndoList = val;
    return(tx->tx_UndoList);
}

/* Called by gc, this makes each undo-lists use less memory than
   max-undo-size. But it always leaves upto the first boundary
   intact.  Doesn't handle the case when the undo list is split
   into three bits while in the middle of a sequence of undo's.  */
void
undo_trim(void)
{
    TX *tx = buffer_chain;
    while(tx)
    {
	VALUE *undo_list;
	long size_count = 0;
	if(tx->tx_ToUndoList && !NILP(tx->tx_ToUndoList))
	    undo_list = &tx->tx_ToUndoList;
	else if(!NILP(tx->tx_UndoneList))
	    undo_list = &tx->tx_UndoneList;
	else
	    undo_list = &tx->tx_UndoList;
	while(CONSP(*undo_list))
	{
	    VALUE item = VCAR(*undo_list);
	    size_count += sizeof(Cons);
	    if(CONSP(item))
	    {
		size_count += sizeof(Cons);
		if(POSP(VCDR(item)))
		    size_count += sizeof(LPos) * 2;
		else if(STRINGP(VCDR(item)))
		    size_count += STRING_LEN(VCDR(item));
		if(size_count > max_undo_size)
		{
		    /* Truncate the list at the end of this group. */
		    while(CONSP(*undo_list) && !NILP(VCAR(*undo_list)))
			undo_list = &VCDR(*undo_list);
		    *undo_list = sym_nil;
		    break;
		}
	    }
	    else if(POSP(item))
	    {
		size_count += sizeof(LPos);
	    }
	    undo_list = &VCDR(*undo_list);
	}
	tx = tx->tx_Next;
    }
}

void
undo_init(void)
{
    mark_static(&pending_deletion_string);
    INTERN(sym_undo, "undo");
    ADD_SUBR(subr_undo);
    ADD_SUBR(subr_max_undo_size);
    ADD_SUBR(subr_buffer_record_undo);
    ADD_SUBR(subr_buffer_undo_list);
}
