/**************************************************************
 *
 *	CRISP - Custom Reduced Instruction Set Programmers Editor
 *
 *	(C) Paul Fox, 1989, 1990, 1991
 *
 *    Please See COPYLEFT notice.
 *
 *   Code to implement the undo and redo facilities.
 **************************************************************/
# include	"list.h"

/**********************************************************************/
/*   Opcodes  for  the different types of undo which can happen to a  */
/*   buffer.							      */
/**********************************************************************/
enum	undo_opcodes {
	GOTO	= 0,
	INSERT	= 1,
	DELETE	= 2,
	RAISE	= 3,
	DROP	= 4,
	SCRAP	= 5,	/* Operation should do an undo on the scrap */
			/* buffer.				    */
	REPLACE = 6,	/* Same as INSERT+DELETE. Used for translate*/
			/* code.				    */
	START	= 7,	/* Marks the beginning of an undo sequence  */
			/* after a write_buffert().		    */
	SOFT_START=8	/* A special marker for undoing translates  */
			/* in interactive mode.			    */
	};

/**********************************************************************/
/*   Current  file  pointer into the undo file. The undo file starts  */
/*   at position 4 in the file.					      */
/**********************************************************************/
static long	u_offset = 4;

/**********************************************************************/
/*   Current  end  of  file pointer (how many bytes we've written so  */
/*   far).							      */
/**********************************************************************/
static long u_end_of_file = 4;

extern	doing_selfinsert;
static	int	undoing_scrap = FALSE;	/* If TRUE stops us printing 	*/
					/* 'Undone.' twice.		*/
FILE	*undo_fp = NULL;

/**********************************************************************/
/*   The states we can be in when doing an undo or a redo.	      */
/**********************************************************************/
enum undo_state {
	NORMAL,
	REDO,
	UNDO
	} undo_state = NORMAL;

/**********************************************************************/
/*   Following  defines  the  structure  used  to keep track of each  */
/*   type of undo information.					      */
/**********************************************************************/
struct undo {
	enum undo_opcodes	u_opcode;
	int	u_line;
	int	u_col;
	RSIZE	u_length;
	long	u_last;		/* tell() position of previous undo */
	int	u_chain;	/* Non-zero if part of a single	    */
				/* undo.			    */
	cmap_t	*u_cmap;	/* The character map		    */
	};
char	*u_fname;		/* Filename of undo file. Made a global */
				/* so that on systems which dont let us */
				/* remove file until it is closed, we know */
				/* exactly what file to delete.		*/

/**********************************************************************/
/*   Size of each slice of the file that is cached.		      */
/**********************************************************************/
# define	SLICE_SIZE	1024

/**********************************************************************/
/*   How many slices to keep in the cache.			      */
/**********************************************************************/
# define	NUM_SLICES	4

/**********************************************************************/
/*   Round  an  offset  so that we never have aliasing problems with  */
/*   offsets being in more than one slice at any one time.	      */
/**********************************************************************/
# define	ROUNDED_DOWN(off)	(off & ~(SLICE_SIZE-1))

/**********************************************************************/
/*   Structure used to keep track of each slice of the file.	      */
/**********************************************************************/
typedef struct slice_t {
	long	sl_offset;	/* Offset into file.			*/
	int	sl_busy;	/* TRUE when buffer holds a slice.	*/
	char	sl_buf[SLICE_SIZE];
	} slice_t;
slice_t	slice_tbl[NUM_SLICES];

static int done_read = TRUE;	/* Set after doing a read because we need */
				/* to re-seek before writing.		*/
extern int dflag;

/**********************************************************************/
/*   Prototypes							      */
/**********************************************************************/
char *	get_tmpdir();
static	read_last_undo PROTO((struct undo *, undo_info *));
static	undo_check PROTO((void));
static int	sub_undo PROTO((struct undo *, int, undo_info *, int));
static void	re_goto PROTO((struct undo *));
static void	u_write PROTO((struct undo *));
void	u_debug PROTO((struct undo *, char *));
void	undo PROTO((int));
void	uwrite_data PROTO((long, char *, int));
void	uwrite_ctrl PROTO((long, struct undo *));
int	read_pos PROTO((FILE *, char *, int, long));
int	write_pos PROTO((FILE *, char *, int, long));
int	slice_read PROTO((FILE *, char *, int, long));
int	slice_write PROTO((FILE *, char *, int, long));
int	slice_rdwr PROTO((FILE *, char *, int, long, int));
slice_t	*find_slice PROTO((long, int));

void
u_init()
{	static char	temp_file[64];
	char	*get_tmpdir();
	char	*sys_delim(); 
	char	open_mode[10];
	char *mktemp();

	sprintf(temp_file, "%s%sunXXXXXX", get_tmpdir(), sys_delim());
	u_fname = mktemp(temp_file);
	 
	sprintf(open_mode, "%s+", FOPEN_W_BINARY);
	if ((undo_fp = fopen(u_fname, open_mode)) == NULL) {
		printf("Cannot create undo file:\n");
		perror(u_fname);
		cr_exit();
		}
	noinherit(fileno(undo_fp));
	/*-------------------------*/
	/* Ensure ftell(undo_fp) != 0) */
	/*-------------------------*/
	fwrite("Undo", 4, 1, undo_fp);
# if	!defined(CANNOT_UNLINK_OPEN_FILES)
	unlink(u_fname);
# endif
}
char *
get_tmpdir()
{	extern char *ggetenv();
	char *cp;
	
	if ((cp = ggetenv("BTMP")) != NULL)
		return cp;
	if ((cp = ggetenv("TMP")) != NULL)
		return cp;
# if defined(VMS)
	return "[-]";
# else
	if (access("/tmp", 0) == 0)
		return "/tmp";
	return ".";
# endif
}

/**********************************************************************/
/*   Function  called  when crisp is going to exit. We need to close  */
/*   and  unlink  the  undo  file,  but  only  if we are on a system  */
/*   where  we  cannot  remove an open file (i.e. a non-Unix system,  */
/*   such as VMS).						      */
/**********************************************************************/
void
u_close()
{
	if (undo_fp) {
		fclose(undo_fp);
# if	 defined(CANNOT_UNLINK_OPEN_FILES)
		unlink(u_fname);
# endif         
		}
}
void
u_replace(str, bytes_to_delete, bytes_to_insert)
char	*str;
RSIZE	bytes_to_delete;
RSIZE	bytes_to_insert;
{	long	l = (long) bytes_to_insert;
	struct undo undo_buf;

	if (undo_check())
		return;
	if (undo_state == NORMAL)
		curbp->b_nummod++;
	undo_buf.u_opcode = REPLACE;
	undo_buf.u_length = bytes_to_delete;
	u_write(&undo_buf);
	uwrite_data(0L, (char *) &l, sizeof l);
	uwrite_data(0L, str, bytes_to_delete);
}
void
u_insert(n, current_byte_offset)
RSIZE	n;
register int	current_byte_offset;
{	register LINE	*clp;
	register int	line;
	struct undo undo_buf;

	if (n == 0 || undo_check())
		return;

	if (undo_state == NORMAL)
		curbp->b_nummod++;
	undo_buf.u_opcode = INSERT;
	undo_buf.u_length = n;

	u_write(&undo_buf);

	line = *cur_line;
	clp = vm_lock_line(line);
	for ( ; line < curbp->b_numlines && n > 0; line++) {
		RSIZE	x, w;
		x = llength(clp) - current_byte_offset;
		w = x;
		if (x >= n)
			w = n;
		uwrite_data(0L, (char *) &ltext(clp)[current_byte_offset], (int) w);
		if (n -= w) {
			uwrite_data(0L, "\n", 1);
			n--;
			}
		current_byte_offset = 0;
		clp = clp->l_fp;
		}


}
void
u_delete(n)
RSIZE	n;
{	struct undo undo_buf1;
	struct undo undo_buf;
	undo_info *up;
	
	if (n == 0 || undo_check())
		return;
	up = undo_state == UNDO ? &curbp->b_redo : &curbp->b_undo;
	if (doing_selfinsert && up->u_last) {
		if (read_last_undo(&undo_buf1, up))
			goto normal;
		if (undo_buf1.u_opcode == DELETE) {
			extern char character;
# ifdef	DEBUG
			if (dflag & DB_UNDO) {
				trace_log("undo(COLLAPSING, %d + %d => %d)\n",
					undo_buf1.u_length, n, 
					undo_buf1.u_length + n);
				}
# endif
			if ((isalnum(curbp->b_uchar) && isalnum(character)) ||
			    (isspace(curbp->b_uchar) && isspace(character)) ||
			    (curbp->b_uchar == 0x1B || character == 0x1B) ||
			    (ispunct(curbp->b_uchar) && ispunct(character))) {
				undo_buf1.u_length += n;
				uwrite_ctrl(up->u_last, &undo_buf1);
				return;
				}
			}
		}
normal:
	if (undo_state == NORMAL)
		curbp->b_nummod++;
	undo_buf.u_opcode = DELETE;
	undo_buf.u_length = n;
	u_write(&undo_buf);

}
void
u_chain()
{	extern int playing_back;
	if (!playing_back && curbp) {
		curbp->b_undo.u_chain = 0;
		curbp->b_redo.u_chain = 0;
		}
}
void
u_terminate()
{	struct undo undo_buf;
	
	if (undo_check())
		return;
	undo_buf.u_opcode = START;
	u_write(&undo_buf);
}
void
u_soft_start()
{	struct undo undo_buf;
	
	if (undo_check())
		return;
	undo_buf.u_opcode = SOFT_START;
	u_write(&undo_buf);
}
void
u_dot()
{	struct undo undo_buf;

	if (undo_check())
		return;
	undo_buf.u_opcode = GOTO;
	u_write(&undo_buf);
}

void
u_raise()
{	struct undo undo_buf;
	if (undo_check())
		return;
	undo_buf.u_opcode = RAISE;
	u_write(&undo_buf);
}
void
u_drop()
{	struct undo undo_buf;

	if (undo_check() || curbp == NULL)
		return;

	undo_buf.u_opcode = DROP;
	u_write(&undo_buf);
	uwrite_data(0L, (char *) curbp->b_anchor, sizeof *curbp->b_anchor);
}
void
u_scrap()
{	struct undo undo_buf;

	if (undo_check())
		return;
	undo_buf.u_opcode = SCRAP;
	u_write(&undo_buf);
		
}
/**********************************************************************/
/*   Check to see whether we should be doing undo for this buffer.    */
/**********************************************************************/
static int
undo_check()
{
	if (curbp->b_flag & BF_NO_UNDO || undo_fp == NULL)
		return TRUE;

	/***********************************************/
	/*   If  user  isn't  undoing  an  undo  then  */
	/*   terminate  the  redo  chain  so  we dont  */
	/*   confuse user.			       */
	/***********************************************/
	if (undo_state == NORMAL)
		curbp->b_redo.u_last = 0;
	return FALSE;
}

/**********************************************************************/
/*   Following  function  writes  out  an  undo atom, setting up the  */
/*   backward  link  for  the  current  undo item so we can find the  */
/*   previous  item  in  the chain. Also stores the current line and  */
/*   column number (common to all undo operations).		      */
/**********************************************************************/
static void
u_write(undop)
struct undo *undop;
{	undo_info *up;

	if (undo_state == UNDO)
		up = &curbp->b_redo;
	else
		up = &curbp->b_undo;
		
	undop->u_line = *cur_line;
	undop->u_col = *cur_col;
	undop->u_last = up->u_last;
	undop->u_chain = up->u_chain++;
	undop->u_cmap = cur_cmap;
	up->u_last = u_end_of_file;

	u_debug(undop, undo_state == NORMAL ? "normal" :
			undo_state == UNDO ? "undo" : "redo");
			
	uwrite_ctrl(0L, undop);
}
void
u_debug(undop, str)
struct undo *undop;
char	*str;
{
# ifdef	DEBUG
	static char *u_opcodes[] = {
		"GOTO", "INSERT", "DELETE", "RAISE", "DROP", "SCRAP", 
		"REPLACE", "START", "SOFT_START"
		};

	if ((dflag & DB_UNDO) == 0)
		return;
	trace_log("%s(%s, line=%d, col=%d, len=%ld, last=%08lx, chain=%d)\n", 
		str,
		u_opcodes[(int) undop->u_opcode],
		undop->u_line, undop->u_col,
		undop->u_length, undop->u_last, undop->u_chain);
# endif
}
void
undo(count)
int	count;
{	int	undo_mod = argv[1].l_flags == F_INT ? argv[1].l_int : 0;
	int	past_mark = argv[2].l_flags == F_INT ? argv[2].l_int : -1;
	long	num = -1;
	struct undo undo_buf;
	enum undo_state	saved_undo_state = undo_state;
	int	redo = argv[3].l_flags == F_INT;
	undo_info *up = redo ? &curbp->b_redo : &curbp->b_undo;
	
	if (dflag)
		trace_log(redo ? "doing redo\n" : "doing undo\n", (char *) NULL);
	undo_state = redo ? REDO : UNDO;
	do {
		if (sub_undo(&undo_buf, redo, up, past_mark)) {
			undo_state = NORMAL;
			return;
			}
		if (num < 0)
			num = undo_buf.u_chain;
		else if (num && undo_buf.u_chain)
			percentage(num - undo_buf.u_chain, num, "Undoing", "command");
		if (undo_mod && 
		    (undo_buf.u_opcode != INSERT &&
		     undo_buf.u_opcode != DELETE))
			undo_buf.u_chain = 1;
		/***********************************************/
		/*   If    we're    doing    an   interactive  */
		/*   translate,   then  terminate  after  the  */
		/*   last sub-chain.			       */
		/***********************************************/
		if (count && undo_buf.u_opcode == SOFT_START)
			break;
		}
	while (undo_buf.u_chain);
	undo_state = saved_undo_state;
	if (!undoing_scrap && count == 0)
		ewprintf(redo ? "Redone." : "Undone.");
}
static int
sub_undo(undop, redo, up, past_mark)
struct undo *undop;
int	redo;
undo_info *up;
int	past_mark;
{	long pos;
	char	buf[64];
	int	n;
	long	saved_last;
	
	if (up->u_last == 0) {
		ewprintf("Nothing to %s.", redo ? "redo" : "undo");
		curbp->b_flag &= ~BFCHG;
		return -1;
		}
	if (read_last_undo(undop, up))
		return -1;
	pos = up->u_last + sizeof *undop;
	u_debug(undop, "UNDOING");
	saved_last = up->u_last;
	up->u_last = undop->u_last;
	
	/***********************************************/
	/*   Restore  the  character  map,  otherwise  */
	/*   converting    to    and    from   column  */
	/*   positions  may  cause  problems if we're  */
	/*   now  viewing  the  buffer in a different  */
	/*   mode.				       */
	/***********************************************/
	cur_cmap = undop->u_cmap;
	switch (undop->u_opcode) {
	  case REPLACE: {
	  	long	l;
		slice_read(undo_fp, (char *) &l, sizeof l, pos);
		pos += sizeof l;
		re_goto(undop);
		ldelete(l);
		/* Fallthru.. */
	  	}
	  case INSERT:
		curbp->b_nummod--;
		re_goto(undop);
		while (undop->u_length > 0) {
			n = undop->u_length;
			if (n > sizeof buf)
				n = sizeof buf;
			slice_read(undo_fp, buf, n, pos);
			linsert(buf, n);
			pos += n;
			undop->u_length -= n;
			}
		if (undo_state == UNDO)
			re_goto(undop);
		break;
	  case GOTO:
		re_goto(undop);
		break;

	  case DELETE:
		curbp->b_nummod--;
		re_goto(undop);
		ldelete(undop->u_length);
		break;

	  case RAISE:
		raise_anchor();
		break;

	  case DROP: {
	  	ANCHOR	tmp_anchor;
		drop_anchor();
		slice_read(undo_fp, (char *) &tmp_anchor, sizeof tmp_anchor, pos);
		*curbp->b_anchor = tmp_anchor;
		break;
		}
	  case SCRAP:
	  	undoing_scrap = TRUE;
		k_undo();
	  	undoing_scrap = FALSE;
		break;
		
	  case START:
		if (past_mark == 0 || !eyorn("Undo past saved file mark")) {
			up->u_last = saved_last;
			return -1;
			}
	  	break;
	  case SOFT_START:
	  	/***********************************************/
	  	/*   Do  nothing  --  this  is  just a marker  */
	  	/*   for the translate/undo option.	       */
	  	/***********************************************/
	  	break;
	  default:
		ewprintf("undo: opcode error");
		}
	return 0;
}
static void
re_goto(undop)
struct undo *undop;
{	
	win_modify(WFMOVE);
	*cur_line = undop->u_line;
	*cur_col = (u_int16) undop->u_col;
	win_modify(WFMOVE);
}

/**********************************************************************/
/*   Read  last  undo  control structure for a buffer. Used for undo  */
/*   compaction   code.   We   scan  the  undo  cache  table  before  */
/*   resorting  to  reading  from the file. Reading from the file is  */
/*   VERY  BAD,  because  stdio  ends  up doing an awful lot of work  */
/*   for only a few bytes, because of our random access behaviour.    */
/**********************************************************************/
static int
read_last_undo(undop, up)
struct undo *undop;
undo_info *up;
{

	if (slice_read(undo_fp, (char *) undop, sizeof *undop, 
	    up->u_last) != sizeof *undop) {
		ewprintf("undo: I/O error");
		return -1;
		}
	return 0;
}
/**********************************************************************/
/*   Write  arbitrary  text  data  to undo file. Used when we delete  */
/*   text  from  buffer  and we need to save the deleted text for an  */
/*   undo operation.						      */
/**********************************************************************/
void
uwrite_data(offset, buf, len)
long	offset;
char	*buf;
int	len;
{
	if (offset == 0)
		offset = u_end_of_file;

	slice_write(undo_fp, buf, len, offset);

	if (offset == u_end_of_file)
		u_end_of_file += len;
}
/**********************************************************************/
/*   Write  a  control buffer to the undo file. We try and cache the  */
/*   undo  control  buffers  in  memory because we may need to refer  */
/*   to them again.						      */
/**********************************************************************/
void
uwrite_ctrl(offset, up)
long	offset;
struct undo *up;
{
	uwrite_data(offset, (char *) up, sizeof *up);
}

/**********************************************************************/
/*   Function to write data to a file at a specified offset.	      */
/**********************************************************************/
int
write_pos(fp, buf, len, offset)
FILE	*fp;
char	*buf;
int	len;
long	offset;
{	
	if (u_offset != offset || done_read)
		fseek(fp, offset, 0);
		
	if (fwrite(buf, len, 1, fp) != 1)
		return -1;

	u_offset = offset + len;
	done_read = FALSE;
	return 0;
}
/**********************************************************************/
/*   Following  function  reads  from  the  undo  file, but tries to  */
/*   optimise  the  number  of  fseek's because of the random access  */
/*   behaviour  of  the  undo  file.  Stdio  is very inefficient for  */
/*   this type of access.					      */
/**********************************************************************/
int
read_pos(fp, buf, len, offset)
FILE	*fp;
char	*buf;
int	len;
long	offset;
{	int	ret;

	if (offset != u_offset) {
		fseek(fp, offset, 0);
		u_offset = offset;
		}
	ret = fread(buf, len, 1, fp);
	u_offset += len;
	done_read = TRUE;
	return ret;
}

/**********************************************************************/
/*   Function  to  read from a file into a buffer, trying to satisfy  */
/*   the request from the slice cache.				      */
/**********************************************************************/
int
slice_read(fp, buf, len, offset)
FILE	*fp;
char	*buf;
int	len;
long	offset;
{
	return slice_rdwr(fp, buf, len, offset, TRUE);
}
/**********************************************************************/
/*   Function  to  write to a slice in the file, attempting to avoid  */
/*   actually  touching  the  file  until we fill the cache. This is  */
/*   like  stdio  with  buffering  but  allows  us  to have multiple  */
/*   'curent pointers' into the file at the same time.		      */
/**********************************************************************/
int
slice_write(fp, buf, len, offset)
FILE	*fp;
char	*buf;
int	len;
long	offset;
{
	return slice_rdwr(fp, buf, len, offset, FALSE);
}
/**********************************************************************/
/*   Common code for reading and writing.			      */
/**********************************************************************/
int
slice_rdwr(fp, buf, len, offset, read_flag)
FILE	*fp;
char	*buf;
int	len;
long	offset;
int	read_flag;
{	register slice_t *sp;
	int	n;
	int	start_len = len;
	char	*cp;

	while (len > 0) {
		sp = find_slice(offset, read_flag);
		if (sp == NULL)
			return -1;
		cp = &sp->sl_buf[offset - sp->sl_offset];
		n = &sp->sl_buf[SLICE_SIZE] - cp;
		if (n > len)
			n = len;
		if (read_flag)
			memcpy(buf, cp, n);
		else
			memcpy(cp, buf, n);
		len -= n;
		buf += n;
		offset += n;
		}
	return start_len;
}

/**********************************************************************/
/*   Try  and  find  a slice which contains the offset specified. If  */
/*   we  dont  have  one,  then  grab  a free entry and return that,  */
/*   writing  the  old  entry  to  the  file. If we're reading, then  */
/*   we'll  need  to fetch the data from the file if we dont have it  */
/*   in the cache.						      */
/**********************************************************************/
slice_t *
find_slice(offset, read_flag)
register long	offset;
int	read_flag;
{	register slice_t *sp;
	static slice_t *last_sp = slice_tbl;
	static slice_t	*last_slice = slice_tbl;
	register int	i;
	
	sp = last_sp;
	for (i = 0; i < NUM_SLICES; i++) {
		if (sp->sl_busy && offset >= sp->sl_offset && 
		    offset < sp->sl_offset + SLICE_SIZE) {
		    	last_sp = sp;
		    	return sp;
			}
		if (++sp >= &slice_tbl[NUM_SLICES])
			sp = slice_tbl;
		}

	if (last_slice >= &slice_tbl[NUM_SLICES])
		last_slice = slice_tbl;
	if (last_slice->sl_busy) {
		if (write_pos(undo_fp, last_slice->sl_buf, 
			SLICE_SIZE, last_slice->sl_offset))
			return NULL;
		}
	last_slice->sl_offset = ROUNDED_DOWN(offset);
	last_slice->sl_busy = TRUE;
	
	/***********************************************/
	/*   If   we're  reading,  then  we  need  to  */
	/*   refresh this entry from the file.	       */
	/***********************************************/
	if (read_flag)
		read_pos(undo_fp, last_slice->sl_buf, 
			SLICE_SIZE, last_slice->sl_offset);
	return last_slice++;
}

