/*
 *	$Id: cpsencode.c,v 1.1.1.1.4.1 2002/01/17 17:42:52 pwessel Exp $
 *
 *      Copyright (c) 1999-2002 by P. Wessel
 *      See COPYING file for copying and redistribution conditions.
 *
 *      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; version 2 of the License.
 *
 *      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.
 *
 *      Contact info: www.soest.hawaii.edu/wessel
 *--------------------------------------------------------------------*/
/*
 *  cpsencode - Generating Complete PostScript (cps) files that
 *		contain all the commands and the data required
 *		to recreate the plot.
 *
 * Author:	Paul Wessel, SOEST, U. of Hawaii
 * Version:	1.3
 * Date:	01-MAR-2000
 *
 * cpsencode should be called at the end of a shell script that
 * produces a single plot and it will append the script and any
 * required data files to the PostScript file as comments after
 * compressing and encoding the files.
 *
 * e.g.,
 *	 cpsencode Figure_3.csh >> Figure_3.ps
 *
 * To recover the script and the data files, run
 *
 *	cpsdecode Figure_3.ps
 *
 * Some options to choose what kind of encoding and compression exist,
 * see the man page.
 *
 * WIN32: Note we open all files (except input script) in binary mode
 * since we dont know if files are binary or ascii.  We must therefore
 * deal explicitly with CR/LF issues.
 */

#include "cps.h"
#include "uu.h"

/* cpsencode knows about several program names that it can safely
   skip without testing to see if they are data files.  It also
   skips various shell/DOS built-in commands and constructs. */

#define N_GMT_PROGS 87
#ifdef WIN32
#define N_DOS_PROGS 95
#define N_PROGS (N_DOS_PROGS+N_GMT_PROGS)
char *programs[N_PROGS] = {
#include "cpsdos.h"
#include "cpsgmt.h"
};
#define FILE_POS 9
#else
#define N_UNIX_PROGS 64
#define N_PROGS (N_UNIX_PROGS+N_GMT_PROGS)
char *programs[N_PROGS] = {
#include "cpsunix.h"
#include "cpsgmt.h"
};
#define FILE_POS 6
#endif


int encode_file (char *file, int mode, int compress, int encode, int verbose, int do_it);
int new_file (char *file, int *mode, int *executeable, int want_executable);
int is_wildcard (char word[]);
char **wildcard_expand (char word[], int *n);
int get_mode (char *file, int *executable);

char **written;
int n_written = 0, n_alloc = 50;

main (int argc, char **argv)
{
	int i, k = -1, len, n_files, n_bad, n_root = 0, ndir, mode;
	int single_quote_on, double_quote_on, is_file, skip_inline_data, verbose = FALSE, executable;
	int more, error = FALSE, compress = TRUE, encode = TRUE, changed_dir = FALSE, want_executable = FALSE;
	int use_tilde = FALSE, hard_path = FALSE, do_it = TRUE, listing = FALSE;
	char buffer[BUFSIZ];
	char *word, EOF_word[120], previous_word[120], **dir;
	FILE *fp;

	for (i = 1; i < argc; i++) {

		if (argv[i][0] == '-') {
			switch (argv[i][1]) {
				case 'e':
				case 'E':
					encode = FALSE;
					break;
				case 'n':
				case 'N':
					do_it = FALSE;
					listing = TRUE;
					verbose = 2;
					break;
				case 'u':
				case 'U':
					compress = FALSE;
					break;
				case 'v':
				case 'V':
					verbose = TRUE;
					break;
				case 'x':
				case 'X':
					want_executable = TRUE;
					break;
				default:
					error = TRUE;
					break;
			}
		}
		else
			k = i;
	}

	if (k < 0) {
		fprintf (stderr, "cpsencode: No script given!\n");
		error++;
	}

	if (compress && !encode) {
		fprintf (stderr, "cpsencode: ERROR.  Compress requires encoding\n");
		error++;
	}

	if (argc < 2 || error) {
		fprintf (stderr, "cpsencode %s - Embed scripts and data files in a CPS Postscript file\n\n", CPS_VERSION);
		fprintf (stderr, "usage: cpsencode script [-e] [-n] [-u] [-v] [-x] >> psfile\n\n");
		fprintf (stderr, "	 OPTIONS\n");
		fprintf (stderr, "	 -e will NOT encode files. Requires -u [Default will]\n");
		fprintf (stderr, "	 -n will NOT do anything but show which files to encode\n");
		fprintf (stderr, "	 -u will NOT compress files first [Default will]\n");
		fprintf (stderr, "	 -v will report on progress [Default is silent]\n");
		fprintf (stderr, "	 -x will embed executable files [Default skips executables]\n");
		exit (EXIT_FAILURE);
	}

	mode = get_mode (argv[k], &executable);

	if ((fp = fopen (argv[k], "r")) == NULL) {
		fprintf (stderr, "cpsencode: Could not open %s\n", argv[k]);
		exit (EXIT_FAILURE);
	}

	written = (char **) malloc ((size_t)(n_alloc * sizeof (char *)));

	if (do_it) {
		/* Logically terminate PostScript plot with EOF comment */

		printf ("%%%%EOF\n");

		/* Then append information about what is to come */

		printf ("%%%%CPS-I:-------------------------------------------------------------------\n");
		printf ("%%%%CPS-I:\tHere lies shell script and possibly data files\n");
		printf ("%%%%CPS-I:\tMade by cpsencode from master script file %s\n", argv[k]);
		printf ("%%%%CPS-I:\tRun cpsdecode");
		if (!encode) printf (" -e");
		if (!compress) printf (" -u");
		printf (" -v on PostScript file to extract script and files\n");
		printf ("%%%%CPS-I:\tCPS_MARKER = %%\n");	/* New since v 1.3 - older files have %%CPS| */
		printf ("%%%%CPS-I:-------------------------------------------------------------------\n");
	}

	/* Encode the shell script commands in plain ASCII */

	if (listing) {
		fprintf (stderr, "-------------------------------------------------------------------\n");
		fprintf (stderr, "Embedded contents for CPS file produced by script: %s\n", argv[k]);
		fprintf (stderr, "Compressed?: %s\n", (compress) ? "YES" : "NO");
		fprintf (stderr, "Encoded?:    %s\n", (encode) ? "YES" : "NO");
		fprintf (stderr, "CPS Marker:  %%\n");
		fprintf (stderr, "Filenames of embedded files follow:\n");
		fprintf (stderr, "-------------------------------------------------------------------\n");
	}

	if (do_it) {
		if (verbose) fprintf (stderr, "cpsencode: embed %s [COPY]\n", argv[k]);
		printf ("%s%s %o\n", CPS_FLAG, argv[k], mode);
		while (fgets (buffer, BUFSIZ, fp)) printf ("%%%s", buffer);
		rewind (fp);
	}
	else if (listing)
		fprintf (stderr, "%s\n", argv[k]);

	(void) new_file (argv[k], &mode, &executable, want_executable);	/* Mark script as read */

	/* Now scan script for filenames whose contents we must encode */
 
	n_files = 1;
	n_bad = 0;
	skip_inline_data = FALSE;

	while (fgets (buffer, BUFSIZ, fp)) {

		if (buffer[0] == 0) continue;	/* Skip blank lines */

		buffer[strlen(buffer)-1] = 0;	/* Get rid of the newline character */

#ifdef WIN32
		if (!strncmp (buffer, "REM CPS: ", FILE_POS)) {	/* Special file to be included as is */
#else
		if (!strncmp (buffer, "#CPS: ", FILE_POS)) {	/* Special file to be included as is */
#endif
			word = &buffer[FILE_POS];

			/* First check for wild cards */

			if (is_wildcard (word)) {

				/* Wild cards are present - use UNIX ls (or DOS dir) to expand them */
				if (word[0] == '~') use_tilde = TRUE;

				dir = wildcard_expand (word, &ndir);
				for (i = 0; i < ndir; i++) {	/* For each expanded file */
					if (new_file (dir[i], &mode, &executable, want_executable)) {	/* File exists for reading */
						n_bad += encode_file (dir[i], mode, compress, encode, verbose, do_it);
						if (dir[i][0] == DIR_DELIM) n_root++;
#ifdef WIN32
						if (strlen(dir[i]) > 1 && dir[i][1] == ':') hard_path = TRUE;
#endif
						n_files++;
					}
					free ((void *)dir[i]);
				}
				free ((void *)dir);
			}
			else {	/* Perhaps a single file */
				if (new_file (word, &mode, &executable, want_executable)) {	/* File exists for reading */
					n_bad += encode_file (word, mode, compress, encode, verbose, do_it);
					if (word[0] == DIR_DELIM) n_root++;
#ifdef WIN32
					if (strlen(word) > 1 && word[1] == ':') hard_path = TRUE;
#endif
					n_files++;
				}
			}
		}

#ifdef WIN32
		if (!strncmp (buffer, "REM", 3)) continue;	/* Skip DOS comments */
		if (!strncmp (buffer, "DEL", 3)) continue;	/* Skip DOS delete lines */
		if (!strncmp (buffer, "del", 3)) continue;	/* Skip DOS delete lines */
#else
		if (buffer[0] == '#') continue;	/* Skip comments */
		if (!strncmp (buffer, "REM", 3)) continue;	/* Skip DOS comments */
		if (!strncmp (buffer, "rm ", 3)) continue;	/* Skip Unix delete lines */
		if (!strncmp (buffer, "\\rm ", 4)) continue;	/* Skip Unix delete lines */
#endif

		/* Replace spaces inside quotes with underscores.  This ensures that text labels
		   like -B3f1:"The x label": is seen as one word (and thus skipped because of -) */

		single_quote_on = double_quote_on = FALSE;
		for (i = 0; buffer[i]; i++) {
			if (buffer[i] == '`') buffer[i] = ' ';	/* Get rid of backquotes for commands */
			if (buffer[i] == '(' || buffer[i] == ')') buffer[i] = ' ';	/* Get rid of parenthesis */
			if (buffer[i] == '"')  double_quote_on = !double_quote_on;
			if (buffer[i] == '\'') single_quote_on = !single_quote_on;
			if ((double_quote_on || single_quote_on) && buffer[i] == ' ') buffer[i] = '_';
		}

		/* Use strtok to read line one word at the time */

		word = strtok (buffer, " \t");
		more = TRUE;
		while (word && more) {

			len = strlen(word);
			is_file = TRUE;
			if (word[0] == '[' && len == 1) is_file = FALSE;	/* Skip single [ */
			if (word[0] == ']') is_file = FALSE;	/* Skip word starting with ] */
			if (word[0] == '\'') is_file = FALSE;	/* Skip word starting with quote */
			if (word[0] == '"') is_file = FALSE;	/* Skip word starting with quote */
			if (word[0] == '+') is_file = FALSE;	/* Skip plus */
			if (word[0] == '=') is_file = FALSE;	/* Skip equals */
			if (word[0] == '>') is_file = FALSE;	/* Skip redirect operator */
			if (word[0] == '|') is_file = FALSE;	/* Skip pipe */
			if (word[0] == '$') is_file = FALSE;	/* Skip shell variable */
			if (word[0] == '\\') is_file = FALSE;	/* Skip backslash */
			if (word[0] == '&') is_file = FALSE;	/* Skip ampersands */
			if (word[0] == ';') is_file = FALSE;	/* Skip semicolons */
			if (word[0] == '!') is_file = FALSE;	/* Skip exclamations */
			if (word[0] == '@') is_file = FALSE;	/* Skip at signs */
			if (word[0] == '%') is_file = FALSE;	/* Skip % signs */
			if (word[0] == '^') is_file = FALSE;	/* Skip hat signs */
			if (word[0] == ':') is_file = FALSE;	/* Skip colons */
			if (word[0] == '#') is_file = more = FALSE;	/* Skip comments */

			if (skip_inline_data && !strcmp (word, EOF_word)) {	/* End of inline data started with << */
				is_file = FALSE;
				skip_inline_data = FALSE;
			}
			else if (!strcmp(word, "<<")) {	/* Beginning of inline data started with << EOF_word */
				strcpy (previous_word, word);
				word = strtok (NULL, " \t");
				is_file = FALSE;
				strcpy (EOF_word, word);
				skip_inline_data = TRUE;
			}
			else if (len > 3 && !strncmp(&word[len-3], ".ps", 3) && previous_word[0] == '>') {	/* Skip PostScript files */
				is_file = FALSE;
			}
			else if (len > 4 && !strncmp(&word[len-4], ".eps", 4) && previous_word[0] == '>') {	/* Skip EPS files */
				is_file = FALSE;
			}

			/* Check if user changes directories */

			if (!strcmp (word, "cd")  || !strcmp (word, "cddir")) changed_dir = TRUE;

			/* Check if word is one of recognized programs - if so skip it */

			for (i = 0; is_file && i < N_PROGS; i++) if (!strcmp (word, programs[i])) is_file = FALSE;

			/* Special check for the convention -?<prefix>.cpt or -?<prefix>.grd which occurs so frequently in GMT */

			if (is_file && word[0] == '-') {	/* Allow files to be passed as option arguments -?<filename> */
				if (len > 2) {
					/* Get rid of the leading option switch and consider this a potential file name */
					i = 2;
					while (word[i]) {
						word[i-2] = word[i];
						i++;
					}
					word[i-2] = '\0';
				}
				else
					is_file = FALSE;
			}

			if (!skip_inline_data && is_file) {
			
				/* word may a readable file(s), if so must encode it(them) in the PostScript file */

				/* First check for wild cards */

				if (is_wildcard (word)) {

					/* Wild cards are present - use UNIX ls (or DOS dir) to expand them */
					if (word[0] == '~') use_tilde = TRUE;

					dir = wildcard_expand (word, &ndir);
					for (i = 0; i < ndir; i++) {	/* For each expanded file */
						if (new_file (dir[i], &mode, &executable, want_executable)) {	/* File exists for reading */
							n_bad += encode_file (dir[i], mode, compress, encode, verbose, do_it);
							if (dir[i][0] == DIR_DELIM) n_root++;
#ifdef WIN32
							if (strlen(dir[i]) > 1 && dir[i][1] == ':') hard_path = TRUE;
#endif
							n_files++;
						}
						free ((void *)dir[i]);
					}
					free ((void *)dir);
				}
				else {	/* Perhaps a single file */
					if (new_file (word, &mode, &executable, want_executable)) {	/* File exists for reading */
						if (word[0] == DIR_DELIM) hard_path = TRUE;
#ifdef WIN32
						if (strlen(word) > 1 && word[1] == ':') hard_path = TRUE;
#endif
						n_bad += encode_file (word, mode, compress, encode, verbose, do_it);
						if (word[0] == DIR_DELIM) n_root++;
						n_files++;
					}
				}
			}

			if (!(len == 1 && word[0] == '\\')) strcpy (previous_word, word);	/* \ is Unix line continue */
			word = strtok (NULL, " \t");
		}
	}
	fclose (fp);

	if (listing) {
		fprintf (stderr, "-------------------------------------------------------------------\n");
		fprintf (stderr, "%d files detected (None were embedded)\n", n_files);
		fprintf (stderr, "-------------------------------------------------------------------\n");
	}

	if (use_tilde) fprintf (stderr, "cpsencode: Filename starting with ~ may not port well\n");
	if (hard_path) fprintf (stderr, "cpsencode: Absolute filenames may not port well\n");
	if (changed_dir) fprintf (stderr, "cpsencode: Script uses cd, may confuse CPS\n");
	if (n_bad) fprintf (stderr, "cpsencode: Unable to encode %d of %d files\n", n_bad, n_files);
	if (n_root) fprintf (stderr, "cpsencode: Warning: %d files had absolute paths\n", n_root);
	if (!listing && verbose) fprintf (stderr, "cpsencode: Encoded %d files\n", n_files);

	for (i = 0; i < n_written; i++) free ((void *)written[i]);

	exit (EXIT_SUCCESS);
}

int encode_file (char *file, int mode, int compress, int encode, int verbose, int do_it)
{
	char cmd[BUFSIZ], line[BUFSIZ];
	FILE *process;

	if (verbose == 2)
		fprintf (stderr, "%s\n", file);
	else if (verbose)
		fprintf (stderr, "cpsencode: embed %s [", file);

	if (compress) {	/* Read the file after compressing it */

		/* We use COMPRESS to compress the file */

		sprintf (cmd, "%s %s\0", COMPRESS, file);
		if (verbose == 1) fprintf (stderr, "COMPRESS");
		if (do_it && (process = popen (cmd, "rb")) == NULL) {
			fprintf (stderr, "cpsencode: Trouble opening process %s\n", cmd);
			return (1);
		}
	}
	else {	/* Just read directly */
		if (do_it && (process = fopen (file, "rb")) == NULL) {
			fprintf (stderr, "cpsencode: Trouble opening file %s\n", file);
			return (1);
		}
	}
	
	if (do_it) printf ("%s%s %o\n", CPS_FLAG, file, mode);	/* Filename marker */
	
	if (encode) {

		/* We use ENCODE to make ASCII */

		if ((verbose == 1) && compress) fprintf (stderr, "+");
		if (verbose == 1) fprintf (stderr, "ENCODE");
		if (do_it) {
			uu_encoder (process, stdout, NULL, file, "%");
			pclose (process);
		}
	}
	else {

		/* Plain ASCII dump */

		if (verbose == 1) fprintf (stderr, "COPY");
		if (do_it) {
			while (fgets (line, BUFSIZ, process)) {
#ifdef WIN32
				line[strlen(line)-2] = 0;	/* Strip off both CR and LF */
				printf ("%%%s\n", line);
#else
				printf ("%%%s", line);
#endif
			}
			fclose (process);
		}
	}
	if (verbose == 1) fprintf (stderr, "]\n");

	if (!do_it) return (0);	/* Just reporting what whould have been done */

	return (0);
}

int new_file (char *file, int *mode, int *executable, int want_executable)
{
	int i, found;

	/* Determines if 1) file exist and 2) if it has not yet been written */

	if (access (file, R_OK)) return 0;	/* Cannot find or read it */
	if (!strcmp(file, "..")) return 0;	/* Parent directory shorthand */
	if (!strcmp(file, ".")) return 0;	/* Current directory shorthand */
#ifdef WIN32
	if (!strcmp(file, "NUL")) return 0;	/* /dev/null */
	if (!strcmp(file, "nul")) return 0;	/* /dev/null */
#else
	if (!strcmp(file, "/dev/null")) return 0;	/* /dev/null */
#endif
	/* Ok, now check if we have read this before */

	for (i = 0, found = FALSE; !found && i < n_written; i++) found = !strcmp (file, written[i]);

	if (found) return (0);	/* Written already */

	/* Must check permissions and ,if necessary, skip executable files */
	
	*mode = get_mode (file, executable);
	if (*mode < 0) return 0;	/* Trouble getting stat - give up */
	if (*executable && !want_executable) return (0);	/* Skip executables */
		
	/* Here we have a new file to be embedded - add it to list of used files */

	if (n_written == n_alloc) {
		n_alloc += 50;
		written = (char **) realloc ((void *)written, (size_t)(n_alloc * sizeof (char *)));
	}
	written[n_written] = (char *) malloc ((size_t)(strlen(file)+1));
	strcpy (written[n_written], file);
	n_written++;

	return (1);
}

int is_wildcard (char word[]) {
	if (strchr (word, '*') || strchr (word, '?') || strchr (word, '[') || strchr (word, ']') || strchr (word, '{') || strchr (word, '}') || (word[0] == '~')) return (TRUE);
	return (FALSE);
}

char **wildcard_expand (char word[], int *n)
{
	char line[BUFSIZ], **flist;
	FILE *ls;
	int n_alloc = 25, len;

#ifdef WIN32
	sprintf (line, "dir /b %s\0", word);
#else
	sprintf (line, "ls %s 2> /dev/null\0", word);
#endif
	if ((ls = popen (line, "r")) == NULL) {
		fprintf (stderr, "cpsencode: Trouble opening process %s\n", line);
		exit (EXIT_FAILURE);
	}
	flist = (char **) malloc ((size_t)n_alloc * sizeof (char *));
	*n = 0;
	while (fgets (line, BUFSIZ, ls)) {	/* For each expanded file */
		len = strlen (line);
		if (line[len-1] == '\n') line[len-1] = '\0';	/* Strip off newline */
		flist[*n] = (char *) malloc (strlen(line)+1);
		strcpy (flist[*n], line);
		(*n)++;
		if ((*n) == n_alloc) {
			n_alloc += 25;
			flist = (char **) realloc ((void *)flist, (size_t)n_alloc * sizeof (char *));
		}
	}
	pclose (ls);
	flist = (char **) realloc ((void *)flist, (size_t)(*n) * sizeof (char *));

	return (flist);
}

int get_mode (char *file, int *executable) {
	struct STAT sbuf;

	if (stat (file, &sbuf)) return -1;	/* Trouble getting stat - give up */
	
	*executable = ((sbuf.st_mode & IS_EXECUTABLE) > 0);
	return (sbuf.st_mode & 0777);
}
