/*
*	crunch.c
*
*	Routines for crunching and decrunching files.
*
*	MWS 3/92.
*/

#include <exec/types.h>
#include <intuition/intuition.h>
#include <libraries/asl.h>
#include <libraries/ppbase.h>
#include <proto/exec.h>
#include <proto/dos.h>
#include <proto/intuition.h>
#include <proto/asl.h>
#include <proto/powerpacker.h>
#include <string.h>

#include "wintext.h"

#include "prefs.h"
#include "messages.h"
#include "crunch.h"
#include "display.h"

/***************************** STATIC PROTOTYPES ******************************/

static void FreeCrunchMem(BOOL display);
static void DoFileRequest(char *hail, char *dir, char *file,
			  LONG flags, void (*func)(char *));
static void OnLoad(char *file);
static void RemExt(char *s, char *ext);
static BOOL AddExt(char *s, char *ext, int destlen);
static BOOL Exists(char *file);
static void SetSaveFile(char *file);
static void OnSave(char *file);
static void OnBatch(char *file);
static void OnDelete(char *file);

/***************************** WINTEXT DATA ***********************************/

static char	filelenbuf[10], decrunchedbuf[10],
		statusbuf[TEXT_COLUMNS+1], progressbuf[TEXT_COLUMNS+1];

		       /* next  text  lp tp pen mode width */
WINTEXT wt_filename =	{ NULL, NULL, 15, 0, 1, JAM2, -1 };
WINTEXT wt_filelength =	{ NULL, filelenbuf, 15, 1, 1, JAM2, -1 };
WINTEXT wt_decrunched =	{ NULL, decrunchedbuf, 15, 2, 1, JAM2, -1 };
WINTEXT wt_status = 	{ NULL, statusbuf,  1, 4, 2, JAM2, -1 };
WINTEXT wt_progress = 	{ NULL, progressbuf,  1, 5, 2, JAM2, -1 };

/***************************** ASL/PPLIB DATA *********************************/

extern struct Window *window;		/* asl requester attached to this */
static struct FileRequester *fr;	/* for load/save/delete operations */

static struct {
	UBYTE *buffer;			/* crunched/decrunched file */
	LONG buflen;			/* size of crunchbuffer */
	LONG crunlen;			/* size of crunched file */
	BOOL saved;			/* has it been saved? */
	BOOL decrunched;		/* does buffer contain decrunched file? */
	LONG efficiency;		/* efficiency used in this context */
} CONTEXT;

#define ERRBUF_LEN	80
static char	errbuf[ERRBUF_LEN];	/* for Fault() */

#define DIR_LEN		100
#define FILE_LEN	40
static char	loaddir[DIR_LEN], loadfile[FILE_LEN],	/* save load/save paths */
		savedir[DIR_LEN], savefile[FILE_LEN];


#define HAILTAG	0
#define DIRTAG	1
#define FILETAG	2
#define FLAGTAG	3	/* used for setting MULTISELECT */

struct TagItem frtags[] = {
	{ ASL_Hail,	NULL },
	{ ASL_Dir,	NULL },
	{ ASL_File,	NULL },
	{ ASL_FuncFlags,NULL },
	{ TAG_DONE }
};

/***************************** INITIATION/FREEING ROUTINES ********************/

BOOL IsBufferSaved()	/* yes or no? */
{
	if (CONTEXT.buffer && !CONTEXT.saved)	/* haven't saved work */
		return FALSE;
	return TRUE;
}

static void FreeCrunchMem(BOOL display)	/* free buffer memory, set pointer to NULL */
{					/* if display == TRUE, update WINTEXTS to */
					/* indicate no file now loaded */

	if (CONTEXT.buffer) FreeMem(CONTEXT.buffer, CONTEXT.buflen);
	CONTEXT.buffer = NULL;

	if (display)	/* set wintexts to blanks (except status line) */
	{
		RenderWinTextsFmt(&wtinfo, &wt_filename, NULL);
		RenderWinTextsFmt(&wtinfo, &wt_filelength, NULL);
		RenderWinTextsFmt(&wtinfo, &wt_decrunched, NULL);
		RenderWinTextsFmt(&wtinfo, &wt_progress, NULL);
	}
}

BOOL InitCrunch()	/* initialize data for crunch routines */
{
	struct TagItem ti[2];

	ti[0].ti_Tag = ASL_Window;
	ti[0].ti_Data = (LONG)window;
	ti[1].ti_Tag = TAG_DONE;

	if (fr = AllocAslRequest(ASL_FileRequest, ti))
		return TRUE;
	return FALSE;
}

void FinishCrunch()	/* free memory/clean up crunch stuff */
{
	FreeCrunchMem(FALSE);
	if (fr) FreeAslRequest(fr);
}

/***************************** GENERAL FILEREQUEST ROUTINE ********************/

static void DoFileRequest(char *hail, char *dir, char *file, LONG flags,
		     void (*func)(char *))
{
	BPTR oldcd, newcd;

	frtags[HAILTAG].ti_Data = (LONG)hail;
	frtags[DIRTAG].ti_Data = (LONG)dir;
	frtags[FILETAG].ti_Data = (LONG)file;
	frtags[FLAGTAG].ti_Data = flags;

	if (AslRequest(fr, frtags))	/* user didn't CANCEL */
	{
		if (flags & FILF_MULTISELECT)	/* batch mode */
		{
			LONG argc = fr->rf_NumArgs;
			struct WBArg *argv = fr->rf_ArgList;

			oldcd = CurrentDir(argv->wa_Lock);  /* get current dir */
			while (argc--)	/* skip through arglist */
			{
				CurrentDir(argv->wa_Lock);
				func(argv->wa_Name);
				argv++;
			}
			CurrentDir(oldcd);
		}
		else if (newcd = Lock(fr->rf_Dir,ACCESS_READ)) /* get lock on directory */
		{
			oldcd = CurrentDir(newcd);	/* move to file's directory */

			if (file) strncpy(file, fr->rf_File, FILE_LEN-1);
			if (dir) strncpy(dir, fr->rf_Dir, DIR_LEN-1);

			func(fr->rf_File);		/* visit file */

			(void)CurrentDir(oldcd);	/* restore current dir */
			UnLock(newcd);
		}
		else Message("Couldn't lock directory %s\n", dir);
	}
}

/***************************** LOAD ROUTINES **********************************/
/* TODO: Add handling of encrypted files? How many use this (I don't)?	      */

BOOL __stdargs __saveds ProgressIndicator(ULONG sofar, ULONG crunlen, ULONG totlen)
{
	struct IntuiMessage *msg;

	RenderWinTextsFmt(&wtinfo, &wt_progress, "%ld%% crunched. (%ld%% gain)",
	     (sofar * 100) / totlen, 100 - (100 * crunlen) / sofar);

	while (msg = (struct IntuiMessage *)GetMsg(window->UserPort))
	{
		if (msg->Class == MENUPICK)
		{
			if ((MENUNUM(msg->Code) == 0) && (ITEMNUM(msg->Code) == 0))
				return FALSE;
		}
		else if (msg->Class == CLOSEWINDOW)
			return FALSE;
	}

	return TRUE;
}

#define PPID 0x50503230		/* 'PP20' */

LONG AlreadyCrunched(char *file)	/* only goes on 1st 2 chars... */
{					/* doesn't recognise encrypted files yet */
	BPTR fh;
	ULONG ppid;
	LONG size = 0;	/* file size, or zero if not crunched */

	if (fh = Open(file, MODE_OLDFILE))
	{
		if (Read(fh, &ppid, sizeof(ppid)) == sizeof(ppid))
		{
			if (ppid == PPID)	/* get old length */
			{
				struct FileInfoBlock __aligned fib;

				if (ExamineFH(fh, &fib))
					size = fib.fib_Size;
			}
		}
		Close(fh);
	}
	/* open/read/examine errors will be picked up later (by ppLoadData) */ 

	return size;
}

static void OnLoad(char *file)	/* main load and (de)crunch routine */
{
	APTR crunchinfo;			/* crunch info for current context */
	LONG oldlen, gainbytes;
	UWORD err;
	BOOL decrunchonly = FALSE;

	if (oldlen = AlreadyCrunched(file))
	{ 				 /*    1        2       0    */ 
		if (prefs.control == QUERY)
			err = MultiRequest("Decrunch|Recrunch|Cancel", 
					"%s is already crunched.\nSelect action:", file);
		else err = (prefs.control + 1)%3;

		if (err == 1)	/* Decrunch */
		{
			RenderWinTextsFmt(&wtinfo, &wt_filelength, "%ld", oldlen);
			decrunchonly = TRUE;
		}
		else if (err == 0) /* Cancel */
		{
			RenderWinTextsFmt(&wtinfo, &wt_status,
					  "Skipping file %.20s", file);
			return;
		}
		/* else err == 2 --> recrunch, so fall through */
	}

	wt_filename.text = file;
	RenderWinTexts(&wtinfo, &wt_filename);
	RenderWinTextsFmt(&wtinfo, &wt_status, "Loading file %.20s", file);

	if (err = ppLoadData(file, prefs.color, 0L,
			     &CONTEXT.buffer, &CONTEXT.buflen, (void *)(-1L)))
	{
		CONTEXT.buffer = NULL;
		RenderWinTextsFmt(&wtinfo, &wt_status, NULL);
		Message("Error loading file %s:\n%s", file, ppErrorMessage(err));
	}
	else if (decrunchonly)	/* Done! update display */
	{
		RenderWinTextsFmt(&wtinfo, &wt_decrunched, "%ld", CONTEXT.buflen);
		RenderWinTextsFmt(&wtinfo, &wt_status, "File decrunched");

		CONTEXT.saved = FALSE;
		CONTEXT.decrunched = TRUE;	/* buffer contains decrunched file */
	}
	else	/* uncrunched file loaded: proceed with crunching */ 
	{
		RenderWinTextsFmt(&wtinfo, &wt_filelength, "%ld", CONTEXT.buflen);

		crunchinfo = ppAllocCrunchInfo( prefs.efficiency,
							prefs.speedup,
							ProgressIndicator, NULL);

		if (crunchinfo)		/* allocation okay */
		{
			CONTEXT.efficiency = prefs.efficiency;
			RenderWinTextsFmt(&wtinfo, &wt_status, "Crunching file %.30s", file);

			SetPPDataMenu(ABORTMENU);
			CONTEXT.crunlen = ppCrunchBuffer(crunchinfo,
							 CONTEXT.buffer, 
							 CONTEXT.buflen);
			SetPPDataMenu(MAINMENU);

			if (CONTEXT.crunlen == PP_CRUNCHABORTED)
			{
				FreeCrunchMem(TRUE);				
				RenderWinTextsFmt(&wtinfo, &wt_status, "Crunch aborted");
			}
			else if (CONTEXT.crunlen == PP_BUFFEROVERFLOW)
			{
				FreeCrunchMem(TRUE);
				RenderWinTextsFmt(&wtinfo, &wt_status, "Buffer overflow");
			}
			else	/* success! Update wintexts */
			{
				gainbytes = CONTEXT.buflen-CONTEXT.crunlen;

				RenderWinTextsFmt(&wtinfo, &wt_decrunched, "%ld", CONTEXT.crunlen);
				RenderWinTextsFmt(&wtinfo, &wt_status, "File crunched");
				RenderWinTextsFmt(&wtinfo, &wt_progress, 
						  "Gained %ld%% (%ld bytes)",
						  (gainbytes * 100) / CONTEXT.buflen,
						  gainbytes);

				CONTEXT.saved = FALSE;
				CONTEXT.decrunched = FALSE; /* buffer contains crunched file */
				/* user can now save buffer at his/her leisure */
			}

			ppFreeCrunchInfo(crunchinfo);

		} /* crunchinfo allocated okay */

	} /* no error on load */
}

void LoadRequest()	/* Load a file; action depends on what file is */ 
{
	RenderWinTextsFmt(&wtinfo, &wt_status, NULL);	/* clear status */

	if (IsBufferSaved() ||
	    Confirm("Current buffer unsaved.\nContinue with load?"))
	{
		FreeCrunchMem(TRUE);	/* free current contents */
  
		DoFileRequest("Load File...", loaddir, loadfile, 0, OnLoad);
	}
}
			
/***************************** SAVE ROUTINES **********************************/

static void RemExt(char *s, char *ext)	/* remove extension if it exists */
{					/* ext SHOULD contain '.' */
	char *t;

	t = &s[strlen(s)-2];		/* t points to last char in s */

	while ((*t != '.') && (t > s))		/* find (last) extension */
		t--;

	if (*t == '.' && !stricmp(t,ext))	/* has given ext */
		*t = '\0';
}

static BOOL AddExt(char *s, char *ext, int destlen)	/* add extension supplied if */
{							/* it's not already on */
	int l;						/* ext SHOULD contain '.' */
	char *t;

	l = strlen(s);		/* length of string */
	t = &s[l-2];		/* t points to last char in s */

	while ((*t != '.') && (t > s))		/* find (last) extension */
		t--;

	if (*t == '.' && !stricmp(t,ext))	/* already has correct ext */
		return TRUE;

	if ((l+strlen(ext)) < destlen)		/* space to append ext? */
	{
		strcat(s,ext);
		return TRUE;
	}

	return FALSE;		/* insufficient space */
}

static BOOL Exists(char *file)			/* does file exist? */
{
	BPTR lock;

	if (lock = Lock(file, ACCESS_READ))
	{
		UnLock(lock);
		return TRUE;
	}
	return FALSE;
}

static void OnSave(char *file)		/* save file from requester */
{
	BPTR fh;
	LONG amount;

	/* TODO: Error messages to get error string from Fault() */

	if (!prefs.overwrite && Exists(file))
		if (!Confirm("Overwrite file %s?", file))
			return;

	amount = CONTEXT.decrunched ? CONTEXT.buflen : CONTEXT.crunlen;

	if (fh = Open(file, MODE_NEWFILE))
	{
		if (CONTEXT.decrunched || ppWriteDataHeader(fh, CONTEXT.efficiency, FALSE, 0))
		{
			if (Write(fh, CONTEXT.buffer, amount) == amount)
			{
				RenderWinTextsFmt(&wtinfo, &wt_status, "Saved to %s", savefile);
				CONTEXT.saved = TRUE;
			}
			else Message("Error writing to file %s", file);
		}
		else Message("Can't write data header to %s", file);

		Close(fh);
	}
	else Message("Can't open file %s", file);
}

static void SetSaveFile(char *name)	/* adds or removes '.pp' depending */
{					/* on CONTEXT and preferences */
	strcpy(savefile, name);
	if (prefs.suffix)
		if (CONTEXT.decrunched)		/* remove possible ".pp" */
			RemExt(savefile, ".pp");
		else
			AddExt(savefile, ".pp", FILE_LEN);
}

void SaveRequest()	/* Save current crunch buffer to file */
{
	RenderWinTextsFmt(&wtinfo, &wt_status, NULL);	/* clear status */
	RenderWinTextsFmt(&wtinfo, &wt_progress, NULL);	/* and progress */

	if (CONTEXT.buffer)	/* something to save */
	{
		SetSaveFile(loadfile);

		DoFileRequest("Save As...", savedir, savefile, FILF_SAVE, OnSave);
	}
	else Message("Nothing to save!");
}

/***************************** BATCH ROUTINES *********************************/

static BOOL savepath;	/* successfully chosen save path? */

static void OnBatch(char *file)	/* process file and save to savedir */
{
	FreeCrunchMem(TRUE);	
	OnLoad(file);		/* do what needs to be done to file */
	if (CONTEXT.buffer)	/* something to save */
	{
		BPTR oldcd, newcd;

		if (newcd = Lock(savedir, ACCESS_READ))
		{
			oldcd = CurrentDir(newcd);
			SetSaveFile(file);
			OnSave(savefile);
			CurrentDir(oldcd);
			UnLock(newcd);
		}
		else Message("Couldn't lock directory %s\n", savedir);
	}
}

static void ObtainSavePath(char *file)	/* note that save path was chosen */
{
	savepath = TRUE;
}

void BatchRequest(void)		/* like the name says... */
{
	if (IsBufferSaved() ||
	    Confirm("Current buffer unsaved.\nProceed with batch?"))
	{
		savepath = FALSE;
		DoFileRequest("Select Save Path...", savedir, NULL,
			      FILF_SAVE, ObtainSavePath);
		if (savepath)
		{
			DoFileRequest("Select files to process",
				      loaddir, loadfile, FILF_MULTISELECT, OnBatch);
		}
	}
}

/***************************** DELETE ROUTINES ********************************/

static void OnDelete(char *file)	/* delete given file */
{
	if (Confirm("Really delete %s?", file))	/* confirm with user */
		if (!DeleteFile(file))		/* do deletion */
		{
			Fault(IoErr(), " - ", errbuf, ERRBUF_LEN);
			Message("Couldn't delete file %s\n%s", file, errbuf); 
		}			
}

void DeleteRequest()	/* Delete a file */
{
	DoFileRequest("Delete File...", loaddir, loadfile, 0, OnDelete);
}
