/*
 *	fhist - file history and comparison tools
 *	Copyright (C) 1991-1994, 1998-2002 Peter Miller;
 *	All rights reserved.
 *
 *	Derived from a work
 *	Copyright (C) 1990 David I. Bell.
 *
 *	This program 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 of the License, or
 *	(at your option) any later version.
 *
 *	This program 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 this program; if not, write to the Free Software
 *	Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111, USA.
 *
 * MANIFEST: functions to extract versions of a file
 */

#include <ac/libintl.h>
#include <ac/stdio.h>
#include <ac/string.h>
#include <ac/unistd.h>

#include <cmalloc.h>
#include <compare.h>
#include <error_intl.h>
#include <extract.h>
#include <fcheck.h>
#include <fhist.h>
#include <fileio.h>
#include <modlin.h>
#include <output/file.h>
#include <output/stdout.h>
#include <output/quotprindeco.h>
#include <subroutine.h>


/*
 * States for examining history file.
 */
#define S_OUTSIDE 0		/* outside of an edit */
#define S_INSIDE 1		/* just inside of an edit */
#define S_CHANGE 2		/* saw changes in an edit */
#define S_FILE 3		/* saw file reference in an edit */


/*
 * In-memory edit history structure.
 */
typedef struct edit EDIT;
struct edit
{
	EDIT	*e_next;	/* next edit history structure in list */
	EDIT	*e_prev;	/* previous edit history structure in list */
	long	e_count;	/* line count */
	char	*e_text;	/* text line if any */
	long	e_textlen;	/* length of text line */
};


/*
 * Local storage.
 */
static EDIT *editfreelist;	/* free list for edit structures */
static EDIT *editnewbegin;	/* pointer to chunk of new edit structures */
static EDIT *editnewend;	/* pointer to end of new edit structures */
static EDIT edithead;		/* head of edit list */
static EDIT edittail;		/* tail of edit list */
static EDIT newedithead;	/* header of new edit list */
static EDIT newedittail;	/* tail of new edit list */
static long curline;		/* current line number */
static char fromsource;		/* dummy addr to indicate source file lines */


/*
 * Initialize an edit list.
 */

static void
initlist(EDIT *hp, EDIT *tp)
{
	hp->e_count = 0;
	hp->e_textlen = 0;
	hp->e_text = NULL;
	hp->e_prev = NULL;
	hp->e_next = tp;
	tp->e_count = 0x40000000;	/* large count of lines */
	tp->e_text = &fromsource;	/* all from the source file */
	tp->e_textlen = 0;
	tp->e_next = NULL;
	tp->e_prev = hp;
	curline = 1;
}


/*
 * Allocate a new edit structure and insert it in front of the
 * specified edit structure in a list.
 */

static EDIT *
allocedit(EDIT *ep)
{
	EDIT	*np;		/* new edit structure */

	np = editfreelist;
	if (np)
		editfreelist = np->e_next;
	else
	{
		if (editnewbegin == editnewend)
		{
			np = (EDIT *)cm_alloc_and_check(sizeof(EDIT) * EDITALLOCSIZE);
			editnewbegin = np;
			editnewend = np + EDITALLOCSIZE;
		}
		np = editnewbegin++;
	}
	np->e_text = NULL;
	np->e_textlen = 0;
	np->e_count = 0;
	np->e_next = ep;
	np->e_prev = ep->e_prev;
	np->e_prev->e_next = np;
	ep->e_prev = np;
	return np;
}


/*
 * Append a change to the new edit list.
 */

static void
addtolist(FILE *fp, long line, long insertcount, long deletecount)
{
	EDIT	*ep;		/* pointer to current element structure */

	if (curline < line)
	{
		/* edit for reading data from source */
		ep = allocedit(&newedittail);
		ep->e_text = &fromsource;
		ep->e_count = line - curline;
		curline = line;
	}
	if (deletecount > 0)
	{
		/* edit for deleting from source */
		ep = allocedit(&newedittail);
		ep->e_count = deletecount;
		curline += deletecount;
	}
	while (--insertcount >= 0)
	{
		/* edits for inserting new text */
		ep = allocedit(&newedittail);
		ep->e_text = get_a_line(fp, NOSEEK, T_TEXT, sc.historyname);
		ep->e_textlen = sc.linelen;
		ep->e_count = 1;
	}
}


static void
printone(const char *str, EDIT *ep)
{
	char	*blab;

	blab = "insert";
	if (ep->e_text == NULL)
		blab = "delete";
	if (ep->e_text == &fromsource)
		blab = "source";
	fprintf
	(
		stderr,
		"  %s at %08lX: %s %ld\n",
		str,
		(unsigned long)ep,
		blab,
		ep->e_count
	);
}


/*
 * Print out an edit list
 */

static void
printlist(const  char *msg, EDIT *ep)
{
	long	line;
	char	*blab;

	fprintf(stderr, "%s\n", msg);
	line = 1;
	while (ep)
	{
		blab = "insert";
		if (ep->e_text == NULL)
			blab = "delete";
		if (ep->e_text == &fromsource)
			blab = "source";
		fprintf
		(
			stderr,
			"edit at %08lX: line %ld: %s %ld\n",
			(unsigned long)ep,
			line,
			blab,
			ep->e_count
		);
		if ((ep->e_text == NULL) || (ep->e_text == &fromsource))
			line += ep->e_count;
		ep = ep->e_next;
	}
}


/*
 * Remove an edit from an edit list and put it onto the free list.
 * Returns the next edit structure on the list.
 */

static EDIT *
remedit(EDIT *ep)
{	
	EDIT	*np;		/* next edit structure in list */

	np = ep->e_prev;
	np->e_next = ep->e_next;
	np = ep->e_next;
	np->e_prev = ep->e_prev;
	ep->e_next = editfreelist;
	editfreelist = ep;
	return np;
}


/*
 * Routine to merge the current and new edit lists together to form
 * the new current edit list containing both changes.  This works by
 * conceptually walking through each 'line' and determining the result
 * of interactions of deleting, inserting, and copying from the source.
 */

static void
mergelists(void)
{
	EDIT	*op;		/* pointer to current old edit structure */
	EDIT	*np;		/* pointer to current new edit structure */
	long	count;		/* number of lines in common */
	int	selectvalue;	/* select value */

	op = edithead.e_next;
	np = newedithead.e_next;
	if (fc.debugflag)
	{
		printlist("old list", op);
		printlist("new list", np);
		fprintf(stderr, "MERGES:\n");
	}

	while (np != &newedittail)
	{
		count = np->e_count;
		if (op->e_count < count) count = op->e_count;
		selectvalue = 0;
		if (np->e_text == NULL) selectvalue = 1;
		if (np->e_text == &fromsource) selectvalue = 2;
		if (op->e_text == NULL) selectvalue += 3;
		if (op->e_text == &fromsource) selectvalue += 6;
		if (fc.debugflag)
		{
			fprintf(stderr, "case %d:\n", selectvalue);
			printone("op:", op);
			printone("np:", np);
		}

		switch (selectvalue)
		{
		case 0:			/* old inserts and new inserts */
		case 2:			/* old inserts and new from source */
			if (fc.debugflag)
				printone("out:", op);
			op = op->e_next;
			break;

		case 1:			/* old inserts and new deletes */
		case 4:			/* old and new both delete */
		case 7:			/* old from source and new deletes */
			op = allocedit(op);
			op->e_count = np->e_count;
			if (fc.debugflag)
				printone("out:", op);
			op = op->e_next;
			np = np->e_next;
			break;

		case 3:			/* old deletes and new inserts */
			np->e_count -= count;
			if (np->e_count <= 0)
			{
				cm_free(np->e_text - 2);	/* HACK */
				np = np->e_next;
			}
			op->e_count--;
			if (op->e_count <= 0)
				op = remedit(op);
			break;

		case 5:			/* old deletes and new from source */
			np->e_count -= count;
			if (np->e_count <= 0)
				np = np->e_next;
			if (op->e_count == count)
			{
				if (fc.debugflag)
				printone("out:", op);
				op = op->e_next;
				break;
			}
			op = allocedit(op);
			op->e_count = count;
			if (fc.debugflag)
				printone("out:", op);
			op = op->e_next;
			op->e_count -= count;
			break;

		case 6:			/* old from source and new inserts */
			op = allocedit(op);
			op->e_text = np->e_text;
			op->e_textlen = np->e_textlen;
			op->e_count = 1;
			if (fc.debugflag)
				printone("out:", op);
			op = op->e_next;
			np = np->e_next;
			if (op == &edittail)
				break;
			op->e_count--;
			if (op->e_count <= 0)
				op = remedit(op);
			break;

		case 8:			/* old and new both from source */
			np->e_count -= count;
			if (np->e_count <= 0)
				np = np->e_next;
			if (op->e_count <= count)
			{
				if (op != &edittail)
				{
					if (fc.debugflag)
						printone("out:", op);
					op = op->e_next;
				}
				break;
			}
			op = allocedit(op);
			op->e_text = &fromsource;
			op->e_count = count;
			if (fc.debugflag)
				printone("out:", op);
			op = op->e_next;
			if (op != &edittail)
				op->e_count -= count;
			break;
		}
		if (fc.debugflag)
			fprintf(stderr, "\n");
	}
	if (fc.debugflag)
	{
		printlist("Updated old list", edithead.e_next);
		fprintf(stderr, "\n");
	}
}


/*
 * Routine to collect edit scripts from the log file for a given edit.
 * This builds an in-memory edit list which can then be used to generate
 * the specified version of the source file.  The history file has already
 * been opened, the edit number checked, and the first line of the desired
 * edit sequence read and checked.
 */

static void
readedits(FILE *fp, long editnumber)
{
	char	*cp;		/* line of data */
	long	filenumber;	/* file number containing whole edit */
	long	linenumber;
	long	insertcount;
	long	deletecount;
	long	inscc;
	long	delcc;
	long	linecc;
	long	temp;
	short	more;
	short	state;		/* state of current edit */
	int	bin;		/* IGNORED */

	insertcount = 0;
	deletecount = 0;
	state = S_INSIDE;
	more = 1;
	linenumber = 1;
	initlist(&edithead, &edittail);
	initlist(&newedithead, &newedittail);

	/*
	 * Here for each line of the edit history file.
	 * Switch on the type of each line, verifying that the
	 * sequence of types is legitimate.  Convert each change
	 * line into one or more edit structures.  After each
	 * set of changes for an edit has been read, then merge
	 * together the old and new edit lists to form the final
	 * edit list.
	 */
	while (more)
	{
		cp = readlinef(fp, (long *) NULL, 0, sc.historyname, &bin);
		if (cp == NULL)
			fatal_intl(0, i18n("error reading edit"));
		switch (*cp++)
		{

		case T_BEGINEDIT:	/* begin another edit */
			cp = getnumber(cp, &temp);
			if (cp == NULL)
				fatal_intl(0, i18n("no number in begin edit"));
			if (state != S_OUTSIDE)
			{
				sub_context_ty	*scp;

				scp = sub_context_new();
				sub_var_set_long(scp, "Number", temp);
				fatal_intl
				(
					scp,
		      i18n("beginning edit $number without ending previous one")
				);
			}
			state = S_INSIDE;
			if (temp != editnumber + 1)
				fatal_intl(0, i18n("non-consecutive begin edit"));
			editnumber = temp;
			insertcount = 0;
			deletecount = 0;
			linenumber = 1;
			initlist(&newedithead, &newedittail);
			break;

		case T_CHANGE:		/* change some lines */
			if (state == S_OUTSIDE)
				fatal_intl(0, i18n("change line outside of edit"));
			if ((state != S_INSIDE) && (state != S_CHANGE))
				fatal_intl(0, i18n("change line not expected in edit"));
			state = S_CHANGE;
			cp = getnumber(cp, &linecc);
			if (cp == NULL)
				fatal_intl(0, i18n("missing line number in change line"));
			if (linecc < linenumber)
				fatal_intl(0, i18n("decreasing line number in change line"));
			cp = getnumber(cp, &inscc);
			cp = getnumber(cp, &delcc);
			if (cp == NULL)
				fatal_intl(0, i18n("bad line counts in change line"));
			if ((inscc == 0) && (delcc == 0))
				fatal_intl(0, i18n("change line does nothing"));
			addtolist(fp, linecc, inscc, delcc);
			insertcount += inscc;
			deletecount += delcc;
			if (delcc == 0) delcc = 1;
			linenumber = linecc + delcc;
			break;

		case T_TEXT:		/* text line */
			if (state != S_OUTSIDE)
				fatal_intl(0, i18n("text line not within edit"));
			fatal_intl(0, i18n("unexpected text line"));
			break;

		case T_FILE:		/* get results from explicit file */
			if (state == S_OUTSIDE)
				fatal_intl(0, i18n("file line not inside edit"));
			if (state != S_INSIDE)
				fatal_intl(0, i18n("file line not expected in edit"));
			state = S_FILE;
			cp = getnumber(cp, &filenumber);
			if (cp == NULL)
				fatal_intl(0, i18n("missing file number in file line"));
			/* NEEDS MORE WORK */
			break;

		case T_ENDEDIT:		/* end of current edit */
			if (state == S_OUTSIDE)
				fatal_intl(0, i18n("ending edit not yet started"));
			state = S_OUTSIDE;
			cp = getnumber(cp, &temp);
			if (cp == NULL)
				fatal_intl(0, i18n("no edit number at end of edit"));
			if (temp != editnumber)
			{
				sub_context_ty	*scp;

				scp = sub_context_new();
				sub_var_set_long(scp, "Number", editnumber);
				fatal_intl
				(
					scp,
				  i18n("end of edit $number not correct number")
				);
			}
			cp = getnumber(cp, &inscc);
			cp = getnumber(cp, &delcc);
			if (cp == NULL)
				fatal_intl(0, i18n("bad total counts in end of edit line"));
			if (inscc != insertcount)
				fatal_intl(0, i18n("wrong insert count at end of edit"));
			if (delcc != deletecount)
				fatal_intl(0, i18n("wrong delete count at end of edit"));
			more = (editnumber != sc.lastedit);
			mergelists();
			break;

		case T_REMARK:		/* remark line */
			if (state == S_OUTSIDE)
				fatal_intl(0, i18n("remark line outside of edit"));
			break;

		case T_EOF:
			fatal_intl(0, i18n("unexpected end of file line"));

		default:
			fatal_intl(0, i18n("unexpected line in edit"));
		}
	}
	fclose_and_check(fp, sc.historyname);
}


/*
 * Write out the extracted file using the information in the edit list
 * and the current source file.
 *
 * The history (.e) file is binary, because we need to seek in it.
 * The source (.s) file is text, because we don't need to seek in it.
 * The input files are text, by definition.
 * The output files are text, by definition.
 */

static void
writefile(const char *outputname, INFO *info)
{
	FILE	*sf;		/* source file */
	output_ty *of;		/* output file */
	EDIT	*ep;		/* current edit */
	long	line;		/* current line number in source file */
	long	count;
	char	*cp;
	int	bin;

	line = 0;
	sf = opensourcefile();
	if (fc.binary)
	{
		of = output_file_binary_open(outputname);
		of = output_quoted_printable_decode(of, 1);
	}
	else
		of = output_file_text_open(outputname);

	/*
	 * Loop over each of the edit structures applying them to the
	 * source file to produce the output file.
	 */
	for (ep = edithead.e_next; ep; ep = ep->e_next)
	{
		count = ep->e_count;

		/*
		 * If we are deleting lines from the source file,
		 * THEN just skip the required number of lines.
		 */
		if (ep->e_text == NULL)
		{
			skipf(sf, count, sc.sourcename);
			continue;
		}

		/*
		 * If we are inserting lines from the history file,
		 * then insert them from the edit structure.
		 */
		if (ep->e_text != &fromsource)
		{
			if (count != 1)
				fatal_intl(0, i18n("edit command for text has bad count value"));
			cp = ep->e_text;
			sc.linelen = ep->e_textlen;
			if (++line <= sc.modifylines)
				cp = modifyline(cp, &sc.linelen, info);
			output_write(of, cp, sc.linelen);
			continue;
		}

		/*
		 * The final choice is preserving lines from the source file,
		 * so copy the lines from the source file.
		 */
		while (count-- > 0)
		{
			bin = 0;
			cp = readlinef(sf, &sc.linelen, 0, sc.sourcename, &bin);
			if (cp == NULL)
			{
				if (ep != &edittail)
					fatal_intl(0, i18n("premature end of file"));
				output_delete(of);
				return;
			}
			if (bin)
			{
				/*
				 * Because all should be encoded using
				 * quoted printable.
				 */
				binary_fatal(sc.sourcename);
			}
			if (++line <= sc.modifylines)
				cp = modifyline(cp, &sc.linelen, info);
			output_write(of, cp, sc.linelen);
		}
	}
	fatal_intl(0, i18n("unexpected end of edit list"));
}


/*
 * Copy the data from one file to another.
 * This is used when extracting the latest version of a module.
 *
 * The history (.e) file is binary, because we need to seek in it.
 * The source (.s) file is text, because we don't need to seek in it.
 * The input files are text, by definition.
 * The output files are text, by definition.
 */

static void
copyfile(const char *outputname, INFO *info)
{
	FILE	*sf;		/* source file for copying */
	output_ty *of;		/* output file for copying */
	long	linelen;	/* length of line */
	long	line;		/* current line number */
	char	*cp;		/* current line of file */
	int	bin;		/* IGNORED */

	sf = opensourcefile();
	if (fc.binary)
	{
		of = output_file_binary_open(outputname);
		of = output_quoted_printable_decode(of, 1);
	}
	else
		of = output_file_text_open(outputname);
	for (line = 0; ; ++line)
	{
		cp = readlinef(sf, &linelen, 0, sc.sourcename, &bin);
		if (cp == NULL)
			break;
		if (line < sc.modifylines)
		    cp = modifyline(cp, &linelen, info);
		output_write(of, cp, linelen);
	}
	output_delete(of);
	fclose_and_check(sf, sc.sourcename);
}


/*
 * Extract a revision of a module using the edit history.
 */

void
extracthistory(char *editname, char *outputname, int blabflag)
{
	char	*str;		/* string being manipulated */
	long	editnumber;	/* edit number being extracted */
	long	i;		/* temporary value */
	INFO	info;		/* information about edit */
	char	infoline[MAX_INFO]; /* first remark line containing info */
	char	editstr[16];	/* edit number string */
	FILE	*fp;

	fp = openhistoryfile(OHF_READ);
	editnumber = findeditnumber(fp, editname);

	/*
	 * If there is an output file, then check to see if we are overwriting
	 * an old file, and let the user know what is being extracted.
	 */
	if (outputname)
	{
		if (sc.nowriteflag && (access(outputname, 0) == 0))
		{
			if (fc.verbosity)
			{
				sub_context_ty	*scp;

				scp = sub_context_new();
				sub_var_set_charstar(scp, "File_Name", outputname);
				sub_var_set_charstar(scp, "Module", sc.modulename);
				error_intl
				(
					scp,
       i18n("existing file \"$filename\" not overwritten by module \"$module\"")
				);
				sub_context_delete(scp);
			}
			fclose_and_check(fp, sc.historyname);
			return;
		}
		if (fc.verbosity || blabflag)
		{
			sub_context_ty	*scp;

			scp = sub_context_new();
			sub_var_set_charstar(scp, "Module", sc.modulename);
			sub_var_set_long(scp, "Number", editnumber);
			sub_var_set_charstar(scp, "File_Name", outputname);
			if (strcmp(outputname, sc.modulename))
			{
				if (editnumber == sc.firstedit)
				{
					error_intl
					(
						scp,
i18n("extracting initial edit $number of module \"$module\" to file \
\"$filename\"")
					);
				}
				else if (editnumber == sc.lastedit)
				{
					error_intl
					(
						scp,
i18n("extracting latest edit $number of module \"$module\" to file \"$filename\"")
					);
				}
				else
				{
					error_intl
					(
						scp,
     i18n("extracting edit $number of module \"$module\" to file \"$filename\"")
					);
				}
			}
			else
			{
				sub_var_optional(scp, "File_Name");
				if (editnumber == sc.firstedit)
				{
					error_intl
					(
						scp,
		   i18n("extracting initial edit $number of module \"$module\"")
					);
				}
				else if (editnumber == sc.lastedit)
				{
					error_intl
					(
						scp,
		    i18n("extracting latest edit $number of module \"$module\"")
					);
				}
				else
				{
					error_intl
					(
						scp,
			   i18n("extracting edit $number of module \"$module\"")
					);
				}
			}
			sub_context_delete(scp);
		}
	}
	if (checknewfile(outputname))
	{
		fclose_and_check(fp, sc.historyname);
		return;
	}
	startedit(fp, editnumber, infoline);

	/*
	 * Fill in the information structure for line modifying to use.
	 */
	str = infoline;
	while (*str == ' ')
		str++;
	info.i_user = str;
	while (*str && (*str != ' '))
		str++;
	*str++ = '\0';
	while (*str == ' ')
		str++;

	/*
	 * Parse and modify the date:
	 * Incoming:  Sun Sep 16 01:03:52 1985\n
	 * Outgoing:  16-Sep-85
	 */
	str[1] = str[8];
	str[2] = str[9];
	str[3] = '-';
	str[7] = '-';
	str[8] = str[22];
	str[9] = str[23];
	str[10] = '\0';
	if (*++str == ' ')
		str++;
	info.i_date = str;
	str = &editstr[15];
	*str = '\0';
	i = editnumber;
	do
	{
		*(--str) = (i % 10) + '0';
		i /= 10;
	}
		while (i);
	info.i_edit = str;

	/*
	 * If we are extracting the latest edit, then just copy it from
	 * the source file.  Otherwise, do the work of applying edits
	 * from the history file.
	 */
	if (editnumber == sc.lastedit)
	{
		fclose_and_check(fp, sc.historyname);
		copyfile(outputname, &info);
		return;
	}
	readedits(fp, editnumber);
	writefile(outputname, &info);
}


/*
 * Reset editing data for new file.
 */

void
editreset(void)
{
	editfreelist = NULL;
	editnewbegin = NULL;
	editnewend = NULL;
}
